- Chapter
- Chapter 1 — Enterprise Architecture Patterns
- Prerequisites
- Volume II (SRP, DIP); Volume III Chapter 8 (Facade)
- You will be able to
- Organise an application into presentation, application, domain and infrastructure layers, apply the dependency rule (and its inverted, hexagonal form), trace a request through the layers, and enforce layering automatically
- Covers
- Presentation; Application; Domain; Infrastructure
On this page
- 1. Concept / Definition
- 2. Why It Exists
- 3. Core Concepts
- 4. How It Works Internally — Dependency Direction
- 4A. Deep Dive — Enforcing Layers Automatically
- 5. APIs / Features / Syntax
- 6. Practical Examples
- 7. Common Problems & Mistakes
- 8. Performance & Production Considerations
- 9. Architecture & Design Considerations
- 10. Interview Questions & Answers
1Concept / Definition
A layered architecture organises an application into horizontal layers, each with a distinct kind of responsibility, where a layer uses the services of the layer(s) below it and is unaware of those above.

| Layer | Responsibility | Typical Spring components |
|---|---|---|
| Presentation | Accept input, render output, protocol concerns | @RestController, @KafkaListener, request/response DTOs |
| Application | Use cases, orchestration, transactions, security | @Service application services, commands, events |
| Domain | Business concepts, rules and invariants | Entities, value objects, aggregates, domain services |
| Infrastructure | Technical capabilities and external systems | Repositories (JPA), HTTP clients, messaging, files |
2Why It Exists
Without layers, a controller computes discounts with SQL, an entity sends emails, and a scheduled job copies half a service's logic. Every change touches unrelated concerns and nothing can be tested in isolation. Layering (popularised for enterprise systems by Fowler and Evans) separates concerns so that each can change for its own reasons: a new API version touches presentation, a new rule touches the domain, a database migration touches infrastructure.
3Core Concepts
3.1 Presentation
The presentation layer adapts external protocols (HTTP, messaging, CLI, UI) to the application.
@RestController
@RequestMapping("/api/v1/orders")
class OrderController {
private final OrderService orders; // application layer only
OrderController(OrderService orders) { this.orders = orders; }
@PostMapping
ResponseEntity<OrderResponse> place(@Valid @RequestBody PlaceOrderRequest req) {
OrderId id = orders.place(req.toCommand()); // DTO → command
return ResponseEntity.created(URI.create("/api/v1/orders/" + id.value()))
.body(orders.view(id)); // result DTO
}
}| Belongs in presentation | Does not belong |
|---|---|
Request parsing, validation of shape (@Valid) | Business rules |
| HTTP status codes, headers, content negotiation | Transactions |
| Mapping requests to commands | Direct repository access |
| Authentication integration (principal extraction) | SQL, entity manipulation |
3.2 Application
The application layer implements use cases: it coordinates domain objects and infrastructure, owns transaction boundaries and applies application-level policies (authorisation, idempotency, events). It is thin — the service layer (Topic 1.2).
@Service
public class OrderService {
private final OrderRepository orders;
private final CustomerRepository customers;
private final ApplicationEventPublisher events;
@Transactional
@PreAuthorize("hasRole('CUSTOMER')")
public OrderId place(PlaceOrder cmd) {
Customer c = customers.get(cmd.customerId());
Order order = Order.place(c, cmd.lines()); // domain decides
orders.save(order);
order.pullEvents().forEach(events::publishEvent);
return order.id();
}
}3.3 Domain
The domain layer holds the business model: entities, value objects, aggregates, domain services and domain events. It expresses what the business means and enforces invariants.
public class Order { // domain entity (aggregate root)
private final OrderId id;
private final CustomerId customer;
private final List<OrderLine> lines = new ArrayList<>();
private OrderStatus status = OrderStatus.PLACED;
public static Order place(Customer customer, List<LineRequest> requested) {
if (customer.isBlocked()) throw new CustomerBlocked(customer.id());
if (requested.isEmpty() || requested.size() > 50) {
throw new InvalidOrderSize(requested.size());
}
var order = new Order(OrderId.newId(), customer.id());
requested.forEach(r -> order.lines.add(OrderLine.of(r)));
order.record(new OrderPlaced(order.id, customer.id(), order.total()));
return order;
}
public Money total() {
return lines.stream().map(OrderLine::subtotal).reduce(Money.ZERO, Money::plus);
}
}| Domain layer rule | Reason |
|---|---|
| No Spring web, no HTTP, no messaging APIs | Rules independent of delivery |
| Persistence annotations minimal or absent | Model shaped by business, not schema (Topic 2.4) |
| No calls to external systems | Use ports/gateways implemented in infrastructure |
| Rich behaviour, not just data | Avoid anemic model (Chapter 2) |
3.4 Infrastructure
Infrastructure provides technical capabilities: persistence, messaging, external APIs, file storage, email.
@Repository
class JpaOrderRepository implements OrderRepository { // implements a domain port
private final SpringDataOrderJpa jpa;
JpaOrderRepository(SpringDataOrderJpa jpa) { this.jpa = jpa; }
public Order get(OrderId id) {
return jpa.findById(id.value()).map(OrderEntity::toDomain)
.orElseThrow(() -> new OrderNotFound(id));
}
public void save(Order order) { jpa.save(OrderEntity.from(order)); }
}Infrastructure is where technology choices live — and therefore where they can be replaced.
4How It Works Internally — Dependency Direction

In classic layering, each layer depends on the one below, so the domain depends on infrastructure (e.g. JPA repositories). In the dependency-inverted form (hexagonal / ports & adapters / clean architecture), the domain defines ports (interfaces such as OrderRepository) and infrastructure implements them — the domain depends on nothing technical.
| Form | Domain depends on | Consequence |
|---|---|---|
| Classic | Infrastructure (JPA, clients) | Simpler; domain harder to test and evolve |
| Inverted | Nothing technical (own interfaces) | Domain testable in memory; infrastructure replaceable |

Strict vs relaxed layering. Strict: a layer may call only the layer directly below. Relaxed: it may call any lower layer. Most Spring applications use relaxed layering for reads (e.g. query services reading projections) and strict layering for writes (through application services and aggregates).
4ADeep Dive — Enforcing Layers Automatically

Layering decays without enforcement. ArchUnit expresses the rules as tests:
@AnalyzeClasses(packages = "com.shop")
class LayeringTest {
@ArchTest
static final ArchRule layers = layeredArchitecture().consideringAllDependencies()
.layer("Presentation").definedBy("..web..")
.layer("Application").definedBy("..application..")
.layer("Domain").definedBy("..domain..")
.layer("Infrastructure").definedBy("..infrastructure..")
.whereLayer("Presentation").mayNotBeAccessedByAnyLayer()
.whereLayer("Application").mayOnlyBeAccessedByLayers("Presentation")
.whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Infrastructure")
.whereLayer("Infrastructure").mayNotBeAccessedByAnyLayer();
@ArchTest
static final ArchRule domainIsFrameworkFree = noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat()
.resideInAnyPackage("org.springframework.web..", "jakarta.servlet..",
"org.apache.kafka..");
}Layers inside modules. In a modular monolith (Spring Modulith), each business module (orders, payments, catalog) has its own layers; modules communicate through application-layer APIs or events. This "vertical slices of horizontal layers" structure scales better than one global set of layers for a large codebase.
| Structure | Package style | Scales to |
|---|---|---|
| Package by layer | web, service, repository, domain | Small applications |
| Package by feature/module, layered inside | orders.web, orders.application, orders.domain, orders.infrastructure | Large applications, modular monoliths |
5APIs / Features / Syntax
| Tool / feature | Role |
|---|---|
| Packages / JPMS modules | Physical grouping and visibility |
| Package-private classes | Hide infrastructure implementations |
Spring stereotypes (@RestController, @Service, @Repository) | Document layer intent; @Repository adds exception translation |
ArchUnit layeredArchitecture() | Enforce dependency rules in tests |
Spring Modulith ApplicationModules.verify() | Enforce module boundaries |
| jMolecules annotations | Express layers/DDD roles explicitly |
6Practical Examples
6.1 Moving a rule to its layer
A controller checked if (order.getStatus() == SHIPPED) return 409. Moving the rule into Order.cancel() (domain) and mapping OrderAlreadyShipped to 409 in a @RestControllerAdvice (presentation) made the rule apply to the Kafka listener and batch job too.
6.2 Package layout for a module
com.shop.orders
├─ web OrderController, PlaceOrderRequest, OrderResponse
├─ application OrderService, PlaceOrder (command), OrderView
├─ domain Order, OrderLine, Money, OrderRepository (port), OrderPlaced
└─ infrastructure JpaOrderRepository, OrderEntity, PaymentGatewayClient6.3 Testing per layer
| Layer | Test style |
|---|---|
| Domain | Plain unit tests, no Spring |
| Application | Unit tests with fake repositories, or @SpringBootTest slices |
| Presentation | @WebMvcTest with mocked application services |
| Infrastructure | @DataJpaTest, Testcontainers, WireMock |
7Common Problems & Mistakes
- Controllers calling repositories directly.
- Returning JPA entities from controllers.
- Business rules in controllers or listeners.
- Domain depending on frameworks (web, messaging).
- Layers as folders only, without dependency rules.
- Pass-through layers that add nothing (a service that just forwards to a repository for every method, forever).
8Performance & Production Considerations
| Aspect | Guidance |
|---|---|
| Call overhead | Layer boundaries are method calls — negligible |
| Mapping costs | DTO/entity mapping is cheap; N+1 queries are not (Topic 3.2) |
| Read paths | Allow query services to read projections directly for heavy reads |
| Transactions | Keep them in the application layer, short and free of remote calls |
9Architecture & Design Considerations
- Prefer dependency-inverted layering for domains with meaningful rules.
- Combine modules (vertical) with layers (horizontal) in larger systems.
- Enforce with ArchUnit/Modulith in CI.
- Accept relaxed layering for reads where it simplifies without leaking rules.
10Interview Questions & Answers
What are the four typical layers of an enterprise application?
A typical enterprise application has four layers. The presentation layer handles interaction with the outside world, such as REST controllers, web pages and message listeners. The application layer coordinates use cases, such as placing an order, managing transactions and authorisation and calling the domain. The domain layer contains the business model and rules, such as orders, prices and invariants. The infrastructure layer provides technical capabilities such as persistence, messaging and calls to external systems. Each layer has a clear responsibility, so changes to, for example, the database or the API format stay mostly within one layer.
What belongs in the presentation layer?
The presentation layer handles everything specific to how the application is called. For a REST API that means controllers, request and response DTOs, mapping between DTOs and application commands or results, validation of the input's shape with Bean Validation, content negotiation and mapping outcomes and exceptions to HTTP status codes and error bodies. Message listeners and scheduled jobs are also presentation-layer entry points. It should not contain business rules or call repositories directly. Keeping it thin means the same use cases can be offered through several channels, and the API can change without touching the domain.
What is the dependency rule in layered architecture?
The dependency rule says that a layer may depend only on layers below it, never on layers above. Presentation depends on application, application on domain, and so on, but the domain never calls a controller or knows about HTTP. In a stricter form, upper layers depend on abstractions that lower layers implement. The rule keeps the core of the system stable: changes in the user interface or in technical details do not ripple into business logic. It also prevents cycles between layers, which would make them impossible to change or test independently. Tools such as ArchUnit can enforce it automatically.
How does hexagonal architecture change classic layering?
In classic layering, the domain sits above infrastructure and depends on it, for example on JPA repositories. Hexagonal architecture inverts that dependency. The domain and application core define ports, which are interfaces describing what they need, such as OrderRepository or PaymentGateway, and infrastructure adapters implement those ports with specific technologies. Inbound adapters, such as controllers, call the core through its use-case ports. The core therefore depends on nothing technical, can be tested without databases or web servers, and can switch technologies by replacing adapters. Layers still exist, but dependencies all point inward towards the domain.
Why should controllers not call repositories directly?
A controller that calls a repository directly skips the application layer, where use-case logic lives. Transactions, authorisation checks, business validations and domain events are then either missing or reimplemented in the controller. When the same operation is later exposed through another entry point, such as a message listener or batch job, the rules must be duplicated, and they drift apart. Controllers also become harder to test and grow into large classes mixing HTTP and business logic. Routing every operation through an application service keeps rules in one place and makes the controller a thin translation layer.
How do you enforce layering in a Spring project?
Write the rules as automated tests that run in the build. ArchUnit's layeredArchitecture rule can declare the layers by package and state which layers may access which, so any violating import fails the build. Additional ArchUnit rules can forbid framework dependencies in the domain, for example no Spring or JPA annotations in domain packages. Spring Modulith can verify module boundaries and dependencies between application modules and generate documentation from them. Combine these with code review guidelines for new dependencies. Automated checks are essential, because layering erodes gradually through small shortcuts that each look harmless.
Package by layer or by feature?
For anything beyond a small application, package by feature or module first, and use layers inside each module. A layer-first structure, with top-level packages for controllers, services and repositories, scatters each feature across the whole code base and makes module boundaries invisible. A feature-first structure, such as orders, payments and catalog packages, each containing its own web, application, domain and infrastructure sub-packages, keeps related code together, makes ownership clear and allows package-private visibility to hide internals. It also prepares the code for modularisation tools such as Spring Modulith, and for extracting a module into a separate service later.
How does layering support evolving a monolith into services?
Good layering makes a monolith much easier to split. If each module exposes its capabilities through an application-layer API, such as an OrderService interface, and its infrastructure dependencies are inverted behind ports, then extracting the module as a service mostly means replacing in-process calls with remote calls, such as REST or messaging, and moving the module's data to its own database. The domain code moves unchanged. Without layering, business logic is tangled with controllers and data access, so extraction requires a rewrite. That is why a well-layered, modular monolith is often the best first step towards microservices.