Stop Writing Anemic Models: DDD Four-Layer Architecture for Enterprise Projects
This article explains how to implement DDD's four-layer architecture (Interface, Application, Domain, Infrastructure) to map bounded contexts to code structure, eliminate anemic domain models by embedding business logic in aggregates, enforce layer boundaries, and use domain events for cross-context communication, with Java package examples and common pitfalls.
Why DDD Fails: The Modeling-Code Disconnect
Many teams perform event storming and produce detailed domain documents with bounded contexts, aggregates, and domain events. However, when coding begins, they revert to a traditional three-layer controller-service-dao structure. All business logic ends up in service classes while entities become mere data carriers with only getters and setters — the classic anemic domain model . The documentation is archived and never updated, leaving code and domain model completely detached.
Core truth: A bounded context is not equivalent to a microservice, but it must equal the code package boundary . Domain logic within the same bounded context must reside in a single package hierarchy; different bounded contexts must be strictly isolated at the code level, with no direct imports of each other's domain objects .
The four-layer architecture aims to separate business domain logic from interfaces, databases, and external calls , so the domain layer focuses purely on business semantics without awareness of databases, RPC, or HTTP.
DDD Four-Layer Architecture Responsibilities
Interface Layer (Interface) — External Entry Points
Receives external requests, validates parameters, transforms requests, returns results, forwards exceptions.
Contains zero business logic.
Exposes HTTP and RPC interfaces.
Defines input/output DTOs.
Performs parameter and permission validation.
Converts external DTOs into application-layer command objects.
Catches exceptions and wraps unified response bodies.
🚫 Forbidden: Do not write business judgments or rules here; do not manipulate domain objects.
Application Layer (Application) — Business Orchestration
Orchestrates business processes, coordinates domain objects, manages transactions, invokes domain capabilities.
Does not implement business rules.
Acts as a "director" — only schedules, does not write business rules.
Receives commands from the interface layer.
Transaction annotations belong here.
Calls domain-layer aggregates and domain services to perform business actions.
Calls infrastructure to execute repositories and send messages.
Triggers publication of domain events.
🚫 Critical prohibition: Application layer must not contain business rules! For example, order amount validation and inventory deduction rules must be pushed down to the Domain layer.
Domain Layer (Domain) — The Heart of DDD
Carries all business rules and domain logic: aggregate roots, entities, value objects, domain services, domain events.
Expresses only business semantics; knows nothing about databases or third-party interfaces.
This layer is the core defense against anemic models.
aggregate: Aggregate roots, entities, value objects — business state and behavior live here.
service: Domain services handling cross-entity business rules.
event: Domain event definitions.
repository: Repository interfaces (only interfaces; implementations go to infrastructure).
command: Domain commands.
💡 Core principle: The domain layer must not depend on any other layer. It cannot import MyBatis, RPC, or MQ classes — it remains a pure business closed loop.
Infrastructure Layer (Infrastructure) — Technical Implementation
Provides technical implementations for the domain layer: databases, caches, messaging, RPC calls.
Implements repository interfaces defined in the Domain layer.
Contains PO (persistence objects) and mappers.
Implements MQ message sending and receiving.
Wraps third-party RPC calls.
Implements caching.
The domain layer depends on abstractions; all technical details sink to infrastructure, achieving decoupling of business from technology .
Enterprise-Grade Package Structure (Java)
The article includes a diagram showing the package layout. Key points:
Packages are isolated by bounded context (e.g., ordercontext, stockcontext, usercontext) under the domain module.
Contexts must not directly import each other's aggregate objects.
Cross-context interaction uses application service calls or asynchronous domain event communication.
Eliminating Anemic Models: Anti-Pattern vs. Correct Pattern
❌ Anemic Model (Anti-Pattern)
// POJO entity, only get/set, no business behavior
public class Order {
private Long id;
private BigDecimal amount;
// only getter setter
}
// All business logic stuffed into Service
@Service
public class OrderService {
public void createOrder() {
Order order = new Order();
// pile of business judgments and state modifications
}
}✅ DDD Rich Domain Model (Correct)
// Aggregate root with embedded business behavior
public class Order {
private OrderId orderId;
private OrderStatus status;
private Money amount;
// Static factory method for order creation
public static Order <strong>create</strong>(OrderCreateCmd cmd) {
Assert.isTrue(cmd.getAmount().greaterThanZero(),
"Order amount must be greater than 0");
Order order = new Order();
order.status = OrderStatus.CREATED;
order.registerEvent(
new OrderCreatedEvent(order.getOrderId()));
return order;
}
// Order's own business behavior
public void <strong>paySuccess</strong>() {
this.status = OrderStatus.PAID;
this.registerEvent(
new OrderPaidEvent(this.orderId));
}
}Core principle: Business behavior belongs to the aggregate root that owns it; do not blindly migrate logic outward to services.
Common Pitfalls Checklist
❌ Repository interface placed in infrastructure layer → ✅ Define repository interface in Domain; implementation in Infrastructure. Domain depends only on abstraction.
❌ Business rules written in Application layer → ✅ Application layer only orchestrates; business validation and state transitions go to Domain.
❌ Direct import of another bounded context's aggregate objects → ✅ Prefer domain events for async communication; for sync, wrap RPC to fetch data — never manipulate another context's domain entities.
❌ Aggregate root exposes setters, allowing external field mutation → ✅ Close public setters; modify state only through internal business methods to guarantee aggregate consistency.
❌ Mixing PO database objects with domain entities → ✅ POs exist only in Infrastructure; Domain uses independent entities and value objects; convert between PO and domain objects.
❌ One application service stuffed with unrelated business → ✅ Application service granularity matches business use cases — one use case per ApplicationService.
Complete Domain Event Flow Across Layers
① Interface layer receives order placement request.
② Calls Application layer createOrder method, starts transaction.
③ Application layer invokes Order.create() static factory on the aggregate root; internal business validation runs, generating OrderCreatedEvent.
④ Application layer calls repository interface to persist the aggregate root.
⑤ After transaction commits, domain events are collected and published uniformly.
⑥ Infrastructure layer MQ component receives events and sends them to the message queue.
⑦ Other bounded contexts consume the event and continue downstream business.
Domain events originate inside the aggregate root , are emitted after transaction commit , ensuring business and message consistency. They do not live in controllers or mappers.
Summary
DDD four layers: Interface, Application, Domain, Infrastructure. Domain is the core, isolating external technical details and carrying all business rules.
Defeating anemic models: Cohesce business behavior into aggregate root entities; stop letting business logic leak outward into services.
Package structure aligns with bounded contexts: Code isolation between contexts; zero direct imports of each other's aggregates.
Repository interfaces in Domain, implementations in Infrastructure: Achieves business-technology decoupling.
Domain events produced by aggregate roots, published post-transaction: Enables reliable cross-context communication.
Event storming outputs must map to code package structure: Documentation and code must stay bidirectionally aligned; otherwise DDD remains theoretical.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Internet Backend Architecture Stack
Over ten years of hands-on Java backend architecture experience, focusing on sharing mainstream internet technologies such as Java backend, microservices, distributed systems, high concurrency, performance optimization, and source code analysis. Building a platform for Java technologists to exchange and grow, collaborating with peers to advance together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
