Strategic DDD maps the big picture — Bounded Contexts, Context Mapping, team topology — but it does not protect a single module from architectural decay. Without tactical patterns, implementations drift into anemic models where all business logic lives in procedural, stateless services. Tactical DDD fills that gap by encoding business behaviour directly into the structure of the code.

Why Strategic DDD Alone Is Not Enough

Teams that stop at strategic analysis often produce what is called "CRUD disguised as DDD." Entities become bare data structures with getters and setters; services accumulate every piece of business logic. The Bounded Context boundary exists on the architecture diagram but not in the code — nothing inside the module protects its rules.

Tactical patterns solve three concrete problems: they enforce transactional consistency around business invariants, they map business intent directly onto code structure, and they make unit testing straightforward — no database containers or external API stubs required.

The Bridge Between Strategy and Tactics: The Reverse Narrative Method

Rather than starting from class design or database schema, the Reverse Narrative method inverts the thought process. Begin at the desired end state — the business fact that must occur — then step backwards: "What conditions had to be true a fraction of a second earlier for this fact to be legal?" Each step back reveals a business invariant that your code must enforce.

lightbulb

This retrospective analysis is the foundation of Event Storming workshops, where it rapidly discovers complex domain processes. The method filters out information noise and pinpoints precisely which invariants the future code must protect.

Tactical DDD Building Blocks: A Fresh Perspective

Translating a reconstructed process into code structure requires treating each tactical pattern as a behaviour enforcer, not a data container. The key shift is from modelling state to modelling behaviour and protecting invariants.

Aggregate and Aggregate Root

An Aggregate defines a transactional boundary inside which the system guarantees complete, immediate consistency of business rules. The Aggregate Root (AR) is the sole public entry point — no internal object may be modified from outside; every state change must pass through a business method on the AR.

Vaughn Vernon's three golden rules for aggregate design:

  • Design small aggregates. Large aggregates reduce throughput and cause concurrent-write conflicts (record locking). An aggregate should contain only the elements that must remain consistent immediately.
  • Reference other aggregates by identity only. Storing just the ID reduces memory footprint, simplifies loading from persistence, and prevents accidental modification of multiple aggregates in one transaction.
  • Rely on eventual consistency across aggregate boundaries. State changes in other aggregates are triggered asynchronously, typically via published Domain Events.

Entity

An Entity has a unique identity that persists over time, regardless of how much its attributes change. The domain identity does not have to match the technical primary key (e.g., a database auto-increment ID); it should be an explicitly expressed business concept generated at the application layer.

Value Object

A Value Object has no identity and is defined entirely by its attributes. Any operation that would modify its state must return a new instance — the original is never mutated. Three defining properties:

  • Immutability: every modifying operation returns a new instance.
  • Self-validation: the constructor refuses to create an instance in an invalid state (e.g., a Money object cannot be created with a negative amount if business rules forbid it).
  • Behaviour over data: the Value Object encapsulates business operation logic, relieving entities of low-level calculations.

Repository

A Repository is an interface that simulates an in-memory collection of aggregates. The key rule: read and write only whole aggregates through their roots. Creating repositories for internal entities is forbidden — their lifecycle is entirely governed by the Aggregate Root.

Domain Service

A stateless operation representing business logic that does not naturally fit the responsibility of a single aggregate. Domain Services typically coordinate interaction between multiple aggregates or communicate with external systems to make a domain decision.

Factory

Encapsulates the complex creation process of a new aggregate, guaranteeing that it enters its lifecycle in a valid initial state. This keeps construction complexity out of the Aggregate Root itself.

Domain Event

A Domain Event is an explicit record of a fact that occurred in the domain in the past and matters to the business. Naming is strict: past tense is mandatory (OrderPlaced, InventoryReserved), emphasising that the fact is done and irreversible.

In modern DDD, Domain Events are not a side-effect of state changes — they are first-class model elements. They serve as the primary communication mechanism between aggregates, enabling Eventual Consistency and loose coupling between modules. Because the past cannot be changed, Domain Events are by definition immutable.

Specification

The Specification pattern encapsulates complex business rules as reusable predicates (objects returning true/false). Extracting this logic prevents it from leaking across domain services or aggregate methods. A Specification answers whether an object satisfies certain business criteria and is used in three scenarios:

  • Validation: checking whether an object's state satisfies preconditions before an action.
  • Selection (querying): defining filter criteria for collections or database queries.
  • Creation: defining requirements that a newly built object must satisfy.

Module

A Module is the technical and logical boundary grouping related domain concepts. Good module design achieves high internal cohesion with low external coupling. Module names must directly reflect terms from the Ubiquitous Language, not technical patterns: prefer ordering/ and billing/ over services/ and models/.

Building Blocks Summary

  • Aggregate Root — global identity, mutable state, long lifecycle managed by business, persisted directly via a dedicated Repository.
  • Internal Entity — identity local to the AR, mutable, lifecycle dependent on the AR, accessed only through Aggregate Root methods.
  • Value Object — no identity, strictly immutable, transient (lifetime determined by context), stored as integral part of the Entity or AR state.
  • Domain Event — no domain identity (carries its own event ID), strictly immutable, transient (publish–archive cycle), dispatched via event bus or recorded in an Event Store.
  • Domain Service — stateless, not persisted, injected via DI, short-lived (for the duration of an operation).
  • Factory — stateless, not persisted, short-lived (for the duration of creation), pure creator of valid domain structures.
  • Specification — no identity, immutable, short-lived (for the duration of a check), defined as pure logic in application code.

Practical Example: E-Commerce Reservation System

Tactical patterns are best illustrated with a reservation confirmation flow. Here is how the Reverse Narrative reveals the model before a single class is written.

Reverse Narrative on the Analysis Board

  • Domain Event: ReservationConfirmed — the reservation was successfully confirmed.
  • Invariant (Rule): A reservation may be confirmed only when the slot is available, the customer has an active status, and the payment fully covers the total cost.
  • Command: ConfirmReservation — confirm the reservation based on payment status.
  • Responsible Aggregate: Reservation — the Aggregate Root that owns and enforces all invariants.

Domain Model: Key Design Decisions

The following design decisions describe a clean domain model fully isolated from infrastructure concerns (databases, ORM frameworks):

  • Price (Value Object): immutable, created via a static factory method only, self-validating (rejects negative amounts or null currency), exposes business operations add(), multiply(), isGreaterThan() — all returning new instances. equals() and hashCode() compare by value, never by reference.
  • ReservationLine (internal Entity): ties a ProductId to quantity and unit price; exposes calculateTotal() which delegates to the Price VO — no raw arithmetic inside the entity.
  • Reservation (Aggregate Root): constructor validates required fields and at least one item; status starts at PENDING; confirm() enforces all three business invariants (status, customer active, payment covers total) before transitioning to CONFIRMED and registering ReservationConfirmed internally.
  • Domain Events are registered internally and dispatched only after a successful database commit — never before. The application service clears them via clearDomainEvents() afterwards.
  • ReservationRepository (interface): simulates an in-memory collection — findById() and save() only. No repository exists for ReservationLine; its lifecycle belongs entirely to Reservation.
lightbulb

The payment and customerStatus parameters passed to confirm() are pure Value Objects reconstructed by the application service — not live references to other aggregates. This enforces the rule of referencing aggregates by identity only.

Common Anti-patterns

God Aggregates

warning

Placing all products, baskets, and orders inside a single Shop aggregate paralyses the system. At the database level, every modification of any element locks the entire object graph, causing performance bottlenecks, thread blocking, and preventing concurrent writes from scaling. Design small aggregates and connect them only by ID.

Business Logic Leaking to Application Services

warning

An application service that checks aggregate state with if/else blocks and makes business decisions produces an anemic domain model. The application service should only coordinate: fetch the aggregate from the repository, call a business method on it (e.g., reservation.confirm(payment, customerStatus)), then save it back.

Coupling the Domain Model Directly to ORM

warning

Placing JPA/Hibernate annotations directly on domain classes destroys business isolation. ORM frameworks impose technical constraints: required no-arg constructors, prohibition on final fields, deep object references (@OneToMany) — all of which break DDD rules. The domain must be fully infrastructure-independent; database mapping belongs in an infrastructure adapter layer using dedicated mappers and separate database entity objects.

Summary and Conclusions

Tactical DDD is a pragmatic approach to building systems resilient to constant changes in business requirements. The key is not writing classes mechanically, but deeply understanding business processes and designing behaviours rather than data structures. The Reverse Narrative method gives developers and architects a reliable tool for identifying real invariants and natural transactional consistency boundaries.

Eric Evans' Blue Book remains the methodological foundation, while Vaughn Vernon's Red Book and DDD Distilled sharpen the rules for building tactical components correctly. Vlad Khononov's Learning Domain-Driven Design presents a modern synthesis linking architecture, strategy, and tactics. For process analysis and the reverse narrative technique, Alberto Brandolini's Introducing EventStorming is essential reading.

Worth noting: additional knowledge sources

Workshop recordings and materials focused on tactical domain behaviour modelling and Event Storming facilitation techniques.

Sławomir Sobótka — Bottega IT Minds https://bottega.com.pl open_in_new

Publications and recordings analysing the technical pitfalls of aggregate design and invariant modelling in domain-driven architectures.

SoftwareMill Blog — Big Picture Event Storming https://softwaremill.com/blog open_in_new

Practical guides on applying the reverse narrative technique to discover gaps in business processes.

Boldare Blog — Event Storming in Practice https://www.boldare.com/blog open_in_new

Case studies and guides on running Event Storming workshops and translating results into tactical DDD structures.