Object-Oriented Programming (Java)

Chapter 13
Java Application Architecture and Integration

Bachelors Program in IT
Object-Oriented Programming (Java)   •   Bachelor

Java Application Architecture and Integration

Introduction and Context

A Java application is rarely an isolated class or a single executable program. Professional systems connect user interfaces, business rules, databases, external services, message brokers, identity providers, monitoring platforms, and deployment environments. Architecture is the discipline of organizing these parts so that the system remains understandable, testable, secure, scalable, and adaptable.

This chapter examines architectural approaches commonly used in Java applications: layered architecture, hexagonal architecture, event-driven systems, and service-oriented systems. It also introduces REST API design, dependency injection, Spring concepts, messaging, containers, cloud deployment, and the trade-offs of distributed systems.

Architecture is not the same as a framework. Spring Boot can help implement several architectures, but it does not automatically produce a good architecture. Architectural quality depends on the boundaries, responsibilities, dependencies, data ownership, and operational assumptions chosen by the development team.

Learning Objectives

By the end of this chapter, you should be able to:

  • explain the purpose and limitations of major Java application architectures;
  • separate domain, application, infrastructure, and presentation responsibilities;
  • design a REST API with appropriate resources, methods, status codes, and error responses;
  • describe dependency injection and important Spring concepts;
  • compare synchronous REST integration with asynchronous messaging;
  • explain how containers and cloud platforms support deployment;
  • analyse consistency, availability, reliability, observability, and security trade-offs;
  • propose an architecture for a realistic Java application.

1. Architecture as a System of Boundaries

Software architecture defines the significant structures of a system, the relationships among those structures, and the principles governing their evolution. A useful architectural boundary answers four questions:

  1. What responsibility exists inside the boundary?
  2. What information is allowed to cross it?
  3. Which direction may dependencies point?
  4. How can the boundary be tested or replaced?

A well-designed system usually exhibits high cohesion and low coupling. High cohesion means that related responsibilities are kept together. Low coupling means that a change in one component does not require unnecessary changes in others.

Architectural decisions should be recorded explicitly. An architecture decision record may state the problem, considered alternatives, decision, consequences, and conditions under which the decision should be revisited. This prevents important decisions from remaining implicit in code.

Architectural qualities

Important quality attributes include:

  • Maintainability: ease of changing and understanding the system.
  • Testability: ability to test behaviour without expensive external dependencies.
  • Scalability: ability to handle increased workload by adding or strengthening resources.
  • Availability: proportion of required time that a service is operational.
  • Reliability: ability to perform correctly over time and under expected conditions.
  • Security: protection of confidentiality, integrity, authentication, and authorization.
  • Observability: ability to infer internal behaviour from logs, metrics, and traces.
  • Performance: responsiveness and efficient resource use.

These qualities conflict. For example, strong consistency may increase latency; independent deployment may increase operational complexity; and extensive abstraction may improve testability while making simple code harder to follow.

2. Layered Architecture

Layered architecture organizes a system into horizontal layers. A common arrangement is:

  1. Presentation layer — HTTP controllers, views, request validation, and response mapping.
  2. Application layer — use cases, transaction coordination, and workflow orchestration.
  3. Domain layer — business concepts, rules, entities, and value objects.
  4. Infrastructure layer — databases, messaging clients, file systems, and external APIs.

A conventional dependency rule allows the presentation layer to call the application layer, the application layer to use the domain layer, and infrastructure to implement interfaces required by inner layers. The domain should not depend on HTTP, SQL, or a particular vendor SDK.

For example, an online ordering application might contain OrderController, PlaceOrderService, Order, Money, and JpaOrderRepository. The controller translates HTTP input into an application request. The application service coordinates the use case. The domain validates business rules. The repository implementation persists the result.

Layering is easy to teach and works well for many business applications. Its weaknesses appear when layers become merely technical folders. A large “service” class may contain validation, persistence, HTTP calls, and business rules, even though the project appears layered. Another danger is a database-centered design in which domain concepts are shaped entirely by tables and ORM mappings.

3. Hexagonal Architecture and Ports and Adapters

Hexagonal architecture, also called ports and adapters architecture, places the application core at the centre. The core contains domain logic and use cases. It communicates through ports, which are interfaces. Adapters connect those ports to technologies such as HTTP, a relational database, a message broker, or a command-line interface.

There are two useful categories of ports:

  • Driving ports expose capabilities of the application to external actors. A REST controller can drive a PlaceOrderUseCase port.
  • Driven ports represent services required by the application. A LoadCustomerPort or OrderRepository port can be implemented by an SQL adapter.

The dependency direction points inward. The core defines the interfaces it needs, while outer adapters implement them. This is a practical application of the Dependency Inversion Principle.

Hexagonal architecture showing application core, ports and adapters Application Core Use cases and domain rules Driving and driven ports REST adapter Message adapter Database adapter External API adapter
Hexagonal architecture showing application core, ports and adapters
Figure: Hexagonal architecture showing application core, ports and adapters

Figure: Ports and adapters isolate the application core from technology-specific adapters.

Hexagonal architecture improves unit testing because a use case can receive fake implementations of its driven ports. It also makes technology replacement easier. However, interfaces should represent meaningful business capabilities, not be created mechanically for every class. Excessive abstraction can produce a complicated design without a real benefit.

4. Dependency Injection and Spring

Dependency injection (DI) means that an object receives its collaborators from outside rather than constructing them internally. Constructor injection is generally preferred because it makes required dependencies explicit and supports immutable fields.

Without DI:

public class OrderService {
    private final SqlOrderRepository repository = new SqlOrderRepository();
}

With DI:

public class OrderService {
    private final OrderRepository repository;

    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }
}

The second class depends on an abstraction and can be tested with a fake repository. The object that assembles implementations is called the composition root.

Spring’s application context is an object container. It creates, configures, and supplies managed objects called beans. Common annotations include @Component, @Service, @Repository, and @Controller. @Bean declares a bean through a configuration method. Spring Boot adds convention-based configuration, embedded servers, dependency management, and operational features.

A typical request path is:

HTTP request → controller → application service → domain model → repository port → database adapter.

Spring features must not be confused with architectural rules. A class annotated @Service can still contain poor design. The team must decide transaction boundaries, validation responsibility, error mapping, and dependency direction.

Transactions and boundaries

A transaction groups operations that must satisfy a consistency rule. In a monolithic application, a database transaction may cover a complete use case. In a distributed workflow, one transaction usually cannot atomically include several independent databases and remote services. Such workflows require techniques such as an outbox, retries, idempotency, compensation, or a saga.

5. REST API Design

REST is an architectural style based on resources, representations, uniform operations, stateless interaction, and a uniform interface. An HTTP API should model business resources rather than expose arbitrary method names.

Examples include:

  • GET /orders/42 retrieves an order;
  • POST /orders creates an order;
  • PATCH /orders/42 partially changes an order;
  • DELETE /orders/42 requests deletion where permitted.

GET, PUT, and DELETE are normally intended to be idempotent: repeating the same request has the same intended effect as making it once. POST is not generally idempotent, although an idempotency key can allow a server to safely deduplicate retries.

Useful status codes include 200 OK, 201 Created, 202 Accepted, 204 No Content, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Content, and 500 Internal Server Error. A service should return consistent error structures containing a code, message, and correlation identifier, while avoiding sensitive internal details.

API design should address pagination, filtering, sorting, validation, authentication, authorization, rate limiting, versioning, and documentation. OpenAPI can describe endpoints, parameters, schemas, and responses. Versioning may be handled through a URL, header, or compatible evolution. The least disruptive approach is usually additive change: add optional fields and preserve existing meanings.

Example REST controller

@RestController
@RequestMapping("/orders")
class OrderController {
    private final PlaceOrderUseCase useCase;

    OrderController(PlaceOrderUseCase useCase) {
        this.useCase = useCase;
    }

    @PostMapping
    ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest request) {
        OrderResult result = useCase.place(request.toCommand());
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(OrderResponse.from(result));
    }
}

The controller should translate protocols, not implement the main business policy. Global exception handling can convert domain errors into stable HTTP responses.

6. Event-Driven and Messaging Architectures

An event is a record that something significant happened, such as OrderPlaced or PaymentReceived. In an event-driven architecture, producers publish events and consumers react to them. A broker may provide queues, topics, persistence, delivery control, and consumer coordination.

A command asks a specific component to perform an action. An event reports a fact that has already occurred. Confusing these concepts leads to unclear ownership and coupling.

Messaging can be:

  • Point-to-point: one message is processed by one consumer in a competing-consumer group.
  • Publish-subscribe: multiple subscribers receive a published event.
  • Synchronous request-response: the caller waits for a reply.
  • Asynchronous: the producer continues without waiting for processing to finish.

Asynchronous systems improve temporal decoupling and resilience to temporary outages, but they introduce delayed consistency, duplicate delivery, ordering problems, poison messages, and harder debugging. Consumers should therefore be idempotent. A consumer can record processed event identifiers, use a unique business key, or apply an update whose result is safe when repeated.

The transactional outbox pattern addresses a common failure: a service updates its database but crashes before publishing an event. The service writes the business change and an outbox record in the same local transaction. A separate publisher reads unsent outbox records and sends them to the broker. Consumers still need deduplication because publication may be retried.

Dead-letter queues hold messages that cannot be processed after defined attempts. Retry policies should distinguish transient failures, such as temporary network unavailability, from permanent failures, such as invalid data. Unbounded retries can overload a failing dependency.

7. Service-Oriented and Microservice Systems

Service-oriented architecture decomposes capabilities into services that communicate through contracts. A service may be deployed independently and own a bounded business capability. Microservices are a small, independently deployable interpretation of service orientation, but “small” has no universal numerical definition.

A service boundary should reflect business ownership, data ownership, change patterns, and team responsibility. Splitting a system by technical layers—such as a separate controller service, database service, and validation service—often creates distributed complexity without useful autonomy.

A monolith is not inherently poor architecture. A well-modularized monolith can provide strong transaction support, simple local calls, straightforward debugging, and lower operational cost. Microservices may provide independent scaling and deployment, fault isolation, and team autonomy, but require service discovery or stable endpoints, observability, deployment automation, security between services, contract testing, and distributed failure management.

The choice should be evidence-based. If an application has one small team, modest scale, and tightly coupled transactions, a modular monolith is frequently the responsible choice. Services become more attractive when independent release, scaling, regulatory isolation, or organizational autonomy provides measurable value.

8. Distributed-System Trade-offs

A distributed system has multiple processes that communicate over a network. Networks can delay, duplicate, reorder, or lose messages; services can fail independently; and clocks are not perfectly synchronized. Therefore, a remote call is not equivalent to a local method call.

Designers should specify timeout limits, retry behaviour, circuit breaking, fallback behaviour, and failure visibility. A retry without a timeout can exhaust threads. Retrying a non-idempotent operation can create duplicates. Circuit breakers temporarily stop calls to a failing dependency, allowing recovery and protecting resources.

Consistency choices must be explicit. Strong consistency makes recent writes immediately visible according to a defined ordering, but may require coordination and increase latency. Eventual consistency allows replicas to converge later and can improve availability and partition tolerance, but users may temporarily observe stale data. User interfaces should explain states such as “payment pending” rather than pretending that every workflow is immediate.

Useful operational measures include request latency, error rate, throughput, queue depth, consumer lag, database saturation, and availability. Distributed tracing attaches a trace identifier to a request as it crosses services. Logs should be structured and should not contain passwords, tokens, or unnecessary personal data.

9. Containers and Cloud Deployment

A container packages an application and its runtime dependencies in an isolated process environment. It is not a complete virtual machine and shares the host kernel. A Java container image commonly contains a suitable JDK or JRE, the application artifact, configuration conventions, and a non-root runtime user.

A production container should be small, reproducible, patched, and scanned for vulnerabilities. Configuration such as database URLs and credentials should be supplied through environment-specific configuration or a secret-management service, not embedded in the image.

Cloud deployment commonly uses:

  • Infrastructure as a Service: virtual machines and networking are managed by the provider at a lower level.
  • Platform as a Service: the provider manages much of the runtime and scaling platform.
  • Container orchestration: a platform schedules containers, performs health checks, supports rolling updates, and manages service discovery.
  • Serverless execution: the provider manages servers and charges according to execution or resource use, with constraints on runtime and state.

Cloud architecture should consider zones, backups, recovery objectives, autoscaling, cost, vendor dependence, data residency, and observability. Autoscaling cannot solve a saturated database or an inefficient algorithm by itself. A health check should distinguish process liveness from readiness to receive traffic.

10. Security and Governance

Security is an architectural concern, not a final testing phase. Authentication establishes identity; authorization decides whether that identity may perform an action. Authorization should be based on server-side policy and object ownership, not on values supplied by a client.

Important controls include encrypted transport, secure credential storage, least privilege, input validation, output encoding, dependency scanning, audit logging, secret rotation, and protection against common web vulnerabilities. Sensitive information should be minimized, retained only as long as necessary, and handled according to applicable privacy law and institutional policy.

API gateways can centralize TLS termination, rate limiting, authentication integration, and routing, but they should not become a single point containing all business logic. Security decisions that affect a resource should remain enforceable by the owning service.

Artificial intelligence may assist with code generation, test creation, log analysis, or documentation. Generated code must undergo human review, security analysis, license review, and testing. Developers must not submit confidential source code, credentials, or personal data to an AI tool without authorization. AI assistance does not transfer accountability for defects or unsafe behaviour.

11. Applied Case: University Equipment-Rental Platform

The following is an illustrative case. A university lends cameras and laboratory equipment to students. The existing application is a single Java program with database code inside controllers. During examination periods, requests increase sharply, reservations are duplicated, and staff cannot determine whether an item is reserved, collected, or overdue.

Problem analysis

The system has low cohesion because HTTP handling, business rules, and persistence are mixed. It also has unclear ownership of reservation state. A failed notification call can cause a reservation transaction to fail, and there is no reliable audit trail.

Proposed approach

The team first creates a modular monolith with modules for inventory, reservations, members, and notifications. Each module has a domain model, application use cases, ports, and adapters. REST controllers are driving adapters. A relational database adapter implements repository ports. The reservation use case owns rules such as “an item cannot have overlapping active reservations.”

Reservation creation is synchronous because the user needs an immediate acceptance or rejection. After the local transaction commits, an ReservationCreated event is published through a transactional outbox. The notification module consumes the event and sends email asynchronously. If email is unavailable, the reservation remains valid and the message is retried.

The API exposes POST /reservations, GET /reservations/{id}, and cancellation operations. A client-generated idempotency key prevents accidental duplicate creation when a browser retries a request. The server records the key with the resulting reservation and rejects conflicting reuse.

The application is packaged as a container and deployed with separate instances for the web process and worker process. Database backups, structured logs, metrics, tracing, and alerts are added. Secrets are supplied by the deployment platform. Access is controlled by roles and by the relationship between a student and a reservation.

Outcome and learning points

The architecture improves testability because reservation rules can be tested without HTTP or a real database. Notifications no longer block the user-facing transaction. The system still has trade-offs: the notification status is eventually consistent, the outbox requires operational monitoring, and the database remains a scaling consideration. The team deliberately avoids microservices until independent scaling or ownership requires them.

The key lesson is architectural proportionality. The solution introduces boundaries where they solve observed problems, but does not distribute every module across the network.

12. Best Practices and Common Failure Modes

Effective Java architecture commonly follows these practices:

  • begin with business capabilities and use cases rather than frameworks;
  • keep domain rules independent of transport and persistence technologies;
  • use constructor injection and explicit composition;
  • define stable API contracts and validate at boundaries;
  • make retryable operations idempotent;
  • use timeouts and bounded retries for remote calls;
  • publish domain events only when their ownership and meaning are clear;
  • test architecture through unit, integration, contract, and end-to-end tests;
  • automate builds, security checks, container scanning, and deployment;
  • measure operational behaviour instead of relying on assumptions;
  • document important decisions and revisit them when conditions change.

Common failures include adopting microservices for prestige, creating interfaces without a reason, exposing database tables directly as public APIs, placing business rules in controllers, treating asynchronous messaging as automatically reliable, and storing secrets in source control. Another failure is designing for theoretical scale while neglecting correctness, usability, and operational simplicity.

Chapter Summary

Java application architecture organizes responsibilities and controls dependencies so that systems can evolve safely. Layered architecture provides a familiar separation of presentation, application, domain, and infrastructure concerns. Hexagonal architecture strengthens this separation by placing the application core behind driving and driven ports.

Dependency injection supplies collaborators from a composition root and improves testability. Spring provides a container and conventions, but it does not replace architectural judgement. REST APIs should model resources, use appropriate HTTP semantics, validate inputs, return consistent errors, and evolve compatibly.

Event-driven systems and messaging provide temporal decoupling and resilience, but require idempotency, retry policies, dead-letter handling, and observability. Service-oriented and microservice architectures can support independent deployment and scaling, but introduce network failures, distributed consistency, security, and operational costs. Containers and cloud platforms support repeatable deployment, yet require careful treatment of configuration, health, secrets, data, and cost.

A responsible architect chooses the simplest structure that satisfies present and foreseeable needs, makes trade-offs explicit, and treats security, reliability, privacy, and operability as core design requirements.

Key Terms

Architecture: The significant structures, relationships, and principles governing a software system.

Layered architecture: An arrangement that separates presentation, application, domain, and infrastructure responsibilities.

Hexagonal architecture: An architecture that isolates an application core using ports and adapters.

Port: An interface through which the application core communicates with an actor or external capability.

Adapter: A technology-specific component that connects an external system to a port.

Dependency injection: Supplying an object’s collaborators from outside rather than constructing them internally.

Bean: An object managed by the Spring application context.

REST: An architectural style using resources, representations, stateless interactions, and a uniform interface.

Idempotency: A property whereby repeating an operation has the same intended effect as performing it once.

Event: A record that a significant fact has occurred.

Command: A request for a component to perform an action.

Message broker: Infrastructure that accepts, stores, routes, and delivers messages.

Transactional outbox: A pattern that stores a business change and an outgoing event in one local transaction.

Eventual consistency: A model in which replicas may temporarily differ but converge over time.

Microservice: An independently deployable service organized around a business capability.

Container: An isolated process package containing an application and its runtime dependencies.

Observability: The ability to understand internal system behaviour through outputs such as logs, metrics, and traces.

Circuit breaker: A control that temporarily stops calls to a failing dependency.

Composition root: The location where application components and their dependencies are assembled.

Idempotency key: A client-provided identifier used to recognize and safely repeat a request.

Review Questions

Short Answer Questions

  1. What is the main purpose of a domain layer?
  2. Distinguish a port from an adapter in hexagonal architecture.
  3. Why is constructor injection generally preferred?
  4. What is the difference between a command and an event?
  5. Why are timeouts necessary for remote service calls?

Long Answer Questions

  1. Compare layered architecture and hexagonal architecture, including their benefits and limitations.
  2. Explain how a well-designed REST API uses HTTP methods, status codes, validation, and error responses.
  3. Describe the transactional outbox pattern and the failure it addresses.
  4. Analyse the operational consequences of replacing a modular monolith with microservices.
  5. Explain how containers and cloud platforms support deployment while introducing new governance concerns.

Analytical / Application Questions

  1. Design the main components and dependency directions for an online examination-submission system using ports and adapters.
  2. A payment service sometimes times out after charging a card. Propose an API and messaging strategy that limits duplicate charges and provides observable recovery.
  3. A small university department proposes ten microservices for a simple equipment catalogue. Evaluate the proposal and recommend an architecture with justified trade-offs.

Multiple Choice Questions with Answers

Question 1

Which dependency direction best represents hexagonal architecture?

A. The domain depends directly on a web framework

B. Adapters implement ports defined by the application core

C. The database defines all application interfaces

D. Controllers contain all business rules

Correct Answer: B. Adapters implement ports defined by the application core

Question 2

Why is constructor injection commonly preferred in Java applications?

A. It makes required dependencies explicit

B. It prevents all runtime exceptions

C. It automatically creates database tables

D. It removes the need for interfaces

Correct Answer: A. It makes required dependencies explicit

Question 3

Which HTTP method is normally intended to be idempotent?

A. POST

B. GET

C. CONNECT

D. PATCH in every possible design

Correct Answer: B. GET

Question 4

What problem does a transactional outbox primarily address?

A. Slow rendering in a browser

B. A database change succeeding while event publication is lost

C. Invalid Java syntax

D. Excessive image size

Correct Answer: B. A database change succeeding while event publication is lost

Question 5

What is a principal reason for making message consumers idempotent?

A. Messages may be delivered more than once

B. It guarantees zero network latency

C. It eliminates the need for monitoring

D. It prevents all schema changes

Correct Answer: A. Messages may be delivered more than once

Question 6

Which statement best distinguishes authentication from authorization?

A. Authentication determines identity; authorization determines permitted actions

B. Authentication encrypts data; authorization compresses data

C. Authentication is only for databases; authorization is only for browsers

D. They are identical concepts

Correct Answer: A. Authentication determines identity; authorization determines permitted actions

Question 7

What is a major advantage of a modular monolith over microservices?

A. It always scales infinitely

B. It avoids all database failures

C. It usually has simpler local calls and operations

D. It requires no testing

Correct Answer: C. It usually has simpler local calls and operations

Question 8

Which practice is most appropriate for a production container?

A. Embed passwords in the image

B. Run unnecessarily as root

C. Use reproducible images and externalize secrets

D. Disable vulnerability scanning

Correct Answer: C. Use reproducible images and externalize secrets

Question 9

What does a circuit breaker do when a dependency repeatedly fails?

A. It temporarily stops calls to protect the system

B. It permanently deletes the dependency

C. It guarantees the dependency has recovered

D. It converts every request into a database transaction

Correct Answer: A. It temporarily stops calls to protect the system

Question 10

Which architectural quality concerns understanding behaviour through logs, metrics, and traces?

A. Encapsulation

B. Observability

C. Inheritance

D. Serialization

Correct Answer: B. Observability

Object-Oriented Programming (Java)
Chapter 13 of 15