How to Apply Domain-Driven Design: Strategies, Architecture, and Code Samples
This article explains the origins of Domain‑Driven Design, describes strategic concepts such as domain, bounded context and context mapping, explores tactical building blocks like entities, value objects, services, aggregates and factories, and demonstrates practical architecture choices and Java code examples for a payment system.
Background
Eric Evans first introduced the concept of Domain‑Driven Design (DDD) in his 2003 book, describing it as a method to reduce software complexity. Vaughn Vernon expanded the ideas in his 2013 book Implementing Domain‑Driven Design .
Introduction
Although Java is an object‑oriented language, traditional three‑layer architectures often use procedural coding where data models are mere carriers and business logic lives in the service layer. This leads to "anemic domain objects" as Vernon calls them.
Procedural approaches can speed up early development but create technical debt. As business logic grows, code becomes bloated, hard to read, and slows delivery and stability.
To address this, we adopted DDD for both new system construction and legacy refactoring, using strategic and tactical design phases.
Strategic Design
Domain
Domain is business, the things an organization does and everything it contains.
Vernon distinguishes sub‑domains: core, supporting, and generic. For an e‑commerce system, sub‑domains include Order, Invoice, etc., aligning naturally with microservice boundaries.
Bounded Context
Bounded context is an explicit boundary within which a domain model is defined. The same name may have different meanings in different contexts.
com.mycompany.xxxIn practice bounded contexts are usually defined under department packages: com.CompanyName.DepartmentName.BoundedContext Typical secondary package names:
com.mycompany.team.xxx.presentation
com.mycompany.team.xxx.application
com.mycompany.team.xxx.domain.model
com.mycompany.team.xxx.infrastructureContext Mapping Diagram
When adopting DDD, first draw a context‑mapping diagram that shows current bounded contexts and their integration relationships.
Abbreviations used in the diagram:
U – Upstream
D – Downstream
ACL – Anticorruption Layer
OHS – Open Host Service
PL – Published Language
Architecture
DDD does not mandate a specific architecture. Because the core domain lives inside a bounded context, many architectural styles can be used.
Layered Architecture
Traditional layered architecture places the core domain in the domain layer, with presentation above and infrastructure below. Strict layering allows coupling only to the immediate lower layer; relaxed layering permits any upper layer to depend on any lower layer.
Application services coordinate domain objects and should be lightweight, handling transactions, security checks, or event publishing.
Placing repository interfaces in the domain layer while implementations reside in the infrastructure layer violates the Dependency Inversion Principle and can cause circular dependencies.
Hexagonal Architecture
Clients interact with the system through equal ports; adding a new client only requires a new adapter that translates client input into the system’s API.
CQRS
Complex queries (e.g., multi‑condition pagination) are handled by separating command and query models. Commands modify aggregates; queries read from a separate read model updated asynchronously via domain events.
Tactical Design
Common domain‑model building blocks:
Entity
An entity has a unique identity that persists over time even as its attributes change.
Example: a person’s ID card number remains constant regardless of name or address changes.
Value Object
A value object describes an aspect of the domain without its own identity.
Characteristics:
It measures or describes something in the domain.
It can be immutable.
It combines related attributes into a conceptual whole.
When its state changes, it is replaced by a new instance.
It can be compared for equality.
It has no side effects.
Money is a typical value object. A complete representation includes amount, unit, and currency:
{
"amount":"123",
"unit":"Yuan/Fen",
"currency":"CNY/USD"
} public class Money {
private String amount;
private MoneyUnit moneyUnit;
private Currency currency;
public long getFen() {}
public String getYuan() {}
public long getTargetCurrencyFen(BigDecimal exchangeRate) {}
}Domain Service
Domain services encapsulate operations that do not naturally belong to an entity or value object.
They should be stateless, defined by the domain model, and focus on behavior rather than data.
Domain Event
Domain events capture significant occurrences within the domain.
Events may be published locally or to remote bounded contexts, enabling asynchronous processing.
Aggregate
An aggregate is a cluster of related objects treated as a single unit for data changes.
Key rules:
Model true invariants inside the consistency boundary; only one aggregate is modified per transaction.
Design small aggregates with a root entity that contains the minimal necessary attributes or value objects.
Reference other aggregates by their root identifiers rather than by holding direct object references.
Use eventual consistency outside the boundary, often via domain events.
Exceptions may be made for UI convenience, lack of technical mechanisms, global transactions, or query performance concerns.
public class OrderAggrRoot {
private String id;
private Seller seller; // value object snapshot
private Buyer buyer; // value object snapshot
private TradeInfo tradeInfo; // value object snapshot
private Money money; // value object
private OrderStatus status; // value object
private List<TransactionEntity> transactionList; // entities
public boolean isRefundable() {}
public boolean isCanBeCancelled() {}
public void processPayResult(PayResult result) {}
public void processRefundResult(RefundResult result) {}
}Factory
Factories encapsulate complex creation logic for aggregates.
public class CarAggrRoot {
CarAggrRoot() {}
private String id;
private String color;
private Engine engine;
private List<Wheel> wheels;
private Chassis chassis;
}
public class CarFactory {
public static CarAggrRoot genCar(String id, String color, Engine engine,
List<Wheel> wheels, Chassis chassis) {
// validation logic …
return new CarAggrRoot(id, color, engine, wheels, chassis);
}
}Repository
Repositories provide safe storage and retrieval of aggregates, abstracting persistence details.
They hide serialization, deserialization, and data‑source specifics from the domain.
Practice
Payment Core Domain
We divided the payment domain into several sub‑domains:
Core sub‑domain: order and transaction (the payment core).
Channel support sub‑domain: interaction with third‑party payment channels.
Promotion support sub‑domain: discounts and marketing activities.
Risk‑control support sub‑domain: fraud detection and security checks.
Merchant common sub‑domain: merchant data used by multiple sub‑domains.
Context Mapping Diagram
The core order context accesses merchant, risk, channel, and promotion sub‑domains through an anticorruption layer. Offline payment and smart store are external domains accessed via open host services.
Code Implementation
// Order aggregate (domain layer)
public class OrderAggrRoot {
private String id; // order id
private Seller seller; // value object snapshot
private Buyer buyer; // value object snapshot
private TradeInfo tradeInfo; // value object snapshot
private Money money; // value object
private OrderStatus status; // value object
private List<TransactionEntity> transactionList; // entities
public boolean isRefundable() {}
public boolean isCanBeCancelled() {}
public void processPayResult(PayResult result) {}
public void processRefundResult(RefundResult result) {}
} // Pay result model (infrastructure layer, anticorruption)
public class PayResult {
// fields …
public boolean isSuccess() {}
public boolean isFailure() {}
public boolean isUnknown() {}
} // Trade application service (application layer)
public class TradeApplicationServiceImpl {
@Resource private OrderRepository orderRepository;
@Resource private OrderDomainService orderDomainService;
@Resource private TradeConfigClient tradeConfigClient;
@Resource private ChannelRoutingClient routingClient;
@Transactional
public PayResponse pay(PayContext context) {
context.checkParams();
TradeConfig tradeConfig = tradeConfigClient.queryTradeConfig(context);
tradeConfig.check();
OrderAggrRoot orderAggrRoot = orderDomainService.createOrder(context);
orderRepository.add(orderAggrRoot);
PayResult result = routingClient.pay(context.genPayRoutingRequest);
orderAggrRoot.processPayResult(result);
orderRepository.update(orderAggrRoot);
return genResponse();
}
} // Order domain service (domain layer)
public class OrderDomainServiceImpl {
@Resource private PreferentialServiceClient preferentialServiceClient;
public OrderAggrRoot createOrder(BasePayContext context) {
DiscountInfo discountInfo = preferentialServiceClient.queryDiscountInfo();
OrderAggrRoot orderAggrRoot = OrderFactory.createOrder(context, discountInfo);
return orderAggrRoot;
}
} // Anticorruption layer for external discount service
public class PreferentialServiceClient {
@Resource private PreferentialService preferentialService;
public DiscountInfo queryDiscountInfo(DiscountQueryRequest request) {
DiscountResult result = preferentialService.queryDiscount(request.genRequest());
return genDiscountInfo(result);
}
private DiscountInfo genDiscountInfo(DiscountResult result) {}
}External services should be accessed through an anticorruption layer to translate external models into local ones, preserving core domain stability.
Conclusion
After studying DDD for several years, the author began applying it in 2021 to refactor a critical system. Despite challenges such as entity design and aggregate loading, the resulting system is stable, readable, and easy to extend, with clear strategic guidance for future domain divisions.
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.
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.
