Tactical Domain-Driven Design: Bringing Strategy to Code
Tactical DDD transforms business understanding into code through seven core patterns — from entities to domain events — building software that truly reflects the domain.
Join the DZone community and get the full member experience.
Join For FreeIn the previous article, I discussed the most often overlooked aspect of Domain-Driven Design: the strategic side. When it comes to software development, teams tend to rush toward code, believing that implementation will clarify the domain. History shows the opposite — building without understanding the underlying reason or direction often leads to systems that are technically correct but conceptually wrong. As the old Latin root of strategy (strategos, “the art of the general”) suggests, the plan must precede the movement.
Now that we’ve explored the “why” and “what,” it’s time to turn to the “how.” Tactical DDD represents this next step — the process of transforming a well-understood domain into expressive, maintainable code. While strategic design defines boundaries and fosters a shared understanding, tactical design brings those ideas to life within each bounded context.
Tactical DDD focuses on implementing the domain model. It provides a rich vocabulary of design patterns — entities, value objects, aggregates, repositories, and domain services — each serving a precise purpose in expressing business logic. These patterns were not invented from scratch by Eric Evans; instead, they emerged from decades of object-oriented design thinking, later refined to fit complex business domains. The term “entity,” for instance, descends from the Latin “entitas” — “being” — emphasizing identity and continuity through change, while “value objects” recall the algebraic notion of equality by content rather than identity.
What makes tactical DDD essential is its ability to create models that are not only accurate but also resilient to change in an era of a vast amount of tools and architecture patterns, such as distributed systems, microservices, and cloud-native architectures. Without a good direction, we can mislead and generate unnecessary complexity. This layer bridges the conceptual clarity of the strategic model with the practical demands of implementation. Tactical design ensures that business rules are captured in code rather than scattered across services, controllers, or database scripts. It’s about writing software that behaves like the business, not merely one that stores its data.
As the strategic part defines direction, the tactical part defines execution. It consists of seven essential patterns that turn concepts into code.
- Entities – Objects with identity that persist and evolve.
- Value Objects – Immutable objects defined only by their attributes.
- Aggregates – Groups of related entities ensuring consistent boundaries.
- Repositories – Interfaces that abstract persistence of aggregates.
- Factories – Responsible for creating complex domain objects.
- Domain Services – Hold domain logic that doesn’t fit an entity or value object.
- Domain Events – Capture and communicate significant occurrences in the domain.
Each plays a specific role in expressing business logic faithfully within a bounded context. Together, they bring the domain model to life, ensuring that design decisions remain aligned with the business, even as they are implemented deep within the code.
Entities
Entities represent domain objects with a unique identity, or ID, that persists over time, even as their attributes might change. They model continuity — something that remains the same even when its data evolves.
They capture real-world concepts like Order, Customer, or Invoice, where identity defines existence. In e-commerce, an Order remains the same object whether it’s created, updated, or completed.
public class Order {
private final UUID orderId;
private List<OrderItem> items = new ArrayList<>();
private OrderStatus status = OrderStatus.NEW;
public void addItem(OrderItem item) {
items.add(item);
}
}
Value Objects
Value Objects describe elements of the domain that are defined entirely by their values, not by their identity. They are immutable, replaceable, and ensure equality through content.
In practice, value objects like Money, Address, or DateRange make models safer and more precise. For example, a Money object adds two amounts of the same currency, ensuring correctness and immutability.
public record Money(BigDecimal amount, String currency) {
public Money add(Money other) {
if (!currency.equals(other.currency())){
throw new IllegalArgumentException("Currencies must match");
}
return new Money(amount.add(other.amount()), currency);
}
}
Aggregates
Aggregates organize related entities and value objects under a single consistency boundary, ensuring that business rules remain valid. The aggregate root acts as the guardian of its internal state.
A typical example is an Order controlling its OrderItems. All modifications are routed through the root, preserving invariants such as total price and item limits.
public class Order {
private final UUID orderId;
private final List<OrderItem> items = new ArrayList<>();
public void addItem(Product product, int quantity) {
items.add(new OrderItem(product, quantity));
}
public BigDecimal total() {
return items.stream() .map(OrderItem::subtotal).reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
Repositories
Repositories abstract the way aggregates are stored and retrieved, allowing the domain to stay independent of database concerns. They act as in-memory collections that handle persistence transparently.
A repository enables the domain to operate at a higher level, focusing on business logic rather than SQL or API calls. For example, an OrderRepository manages how Order objects are saved or found, without exposing infrastructure details.
public interface OrderRepository {
Optional<Order> findById(UUID id);
void save(Order order);
void delete(Order order);
}
Factories
Factories are responsible for creating complex domain objects while ensuring that all invariants are satisfied. They centralize creation logic, keeping entities free from construction complexity.
When creating an Order, for example, a factory ensures the object starts in a valid state and respects business rules — avoiding scattered creation logic throughout the code.
public class OrderFactory {
public Order create(Customer customer, List<Product> products) {
Order order = new Order(UUID.randomUUID(), customer);
products.forEach(p -> order.addItem(p, 1));
return order;
}
}
Domain Services
Domain Services hold domain logic that doesn’t naturally belong to an entity or value object. They express behaviors that involve multiple aggregates or cross-cutting business rules.
For instance, a PaymentService could coordinate payment processing for an order. It operates at the domain level, preserving the model’s purity while integrating with external systems when necessary.
public class PaymentService {
private final PaymentGateway gateway;
public PaymentService(PaymentGateway gateway) {
this.gateway = gateway;
}
public PaymentReceipt processPayment(Order order, Money amount) {
return gateway.charge(order.getOrderId(), amount);
}
}
Domain Events
Domain Events capture meaningful occurrences within the business domain. They represent something that happened — not an external trigger, but a fact that the domain itself wants to share. This makes the model more expressive, reactive, and aligned with real business language.
For example, when an Order is placed, it can publish an OrderPlacedEvent. Other parts of the system, such as billing, shipping, or notification services, can then react independently, promoting decoupling and scalability.
public record OrderPlacedEvent(UUID orderId, Instant occurredAt) {
public static OrderPlacedEvent from(Order order) {
return new OrderPlacedEvent(order.getOrderId(), Instant.now());
}
}
Application Services — Orchestrating Use Cases
Although Application Services are not part of the original seven tactical DDD patterns, they deserve mention for their role in modern architectures. They act as use-case orchestrators, coordinating domain operations without containing business logic themselves. Application services sit above the domain layer, ensuring that controllers, APIs, or message handlers remain thin and focused on their primary purpose: communication.
For example, when placing an order, an application service coordinates the creation of the Order, its persistence, and the payment process. The domain remains responsible for what happens, while the application service decides when and in which sequence those actions occur.
public class OrderApplicationService {
private final OrderRepository repository;
private final PaymentService paymentService;
private final OrderFactory factory;
public OrderApplicationService(OrderRepository repository,
PaymentService paymentService,
OrderFactory factory) {
this.repository = repository;
this.paymentService = paymentService;
this.factory = factory;
}
@Transactional
public void placeOrder(Customer customer, List<Product> products) {
Order order = factory.create(customer, products);
repository.save(order);
paymentService.processPayment(order, order.total());
}
}
In practice, application services serve as the entry points for use cases, managing transactions, invoking domain logic, and triggering external integrations as needed. They maintain the model’s purity while enabling the system to execute coherent business flows from end to end.
Conclusion
Tactical Domain-Driven Design brings strategy to life. While the strategic side defines boundaries and shared understanding, the tactical patterns — entities, value objects, aggregates, repositories, factories, domain services, and domain events — translate that vision into expressive, maintainable code. Even the application service, although not part of the original seven, plays a vital role in orchestrating use cases and maintaining the model's purity.
Opinions expressed by DZone contributors are their own.
Comments