skip to content

Tactical Design

The building blocks used inside one bounded context: entities, value objects, aggregates, domain events, services, repositories, factories and specifications. This is the level where DDD turns into code you actually write.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

In Domain-Driven Design, what is an aggregate root, and why should code outside the aggregate hold a reference to it by ID rather than by object pointer?

level: juniorimportance: must knowfreq 80%

answer

  1. root = single entry point
  2. ID not object pointer
  3. one repository per aggregate
  4. avoid JPA cascade leaking boundary
  5. consistency boundary = transaction boundary

basics

~20 s

An aggregate root is the single object outside code may touch inside a related cluster. Everything else is reached through it. Other code keeps just its ID, not a direct pointer, so it can't reach in and break the rules.

solid answer

~30 s

An aggregate is a cluster of entities/value objects treated as one consistency unit; the aggregate root is its single entry point — external code (repositories, other aggregates, application services) may only hold and load references to the root, never to internal entities. Referencing by ID instead of object pointer prevents another part of the system from directly mutating internal state and bypassing the root's invariant checks, keeps aggregates independently loadable/persistable, and avoids large object graphs being pulled into memory or serialized together. It also decouples aggregate lifecycles so one aggregate's schema/identity can change without rippling through in-memory references held elsewhere.

go deeper

for a junior

Can state that an aggregate has one root and that other code goes through it; may not yet articulate why ID vs pointer matters for transactions.

for a middle

Explains that ID references prevent bypassing invariants and keep persistence boundaries clean; can name repository-per-aggregate as the pattern.

for a senior

Ties the rule to transaction/locking boundaries, discusses the read-model workaround for cross-aggregate display, and recognizes the JPA cascade pitfall.

for a principal

Reasons about the rule as an architectural constraint that enables independent scaling/sharding of aggregates and informs where eventual consistency (events) must take over from direct calls.

## What an aggregate is An aggregate, in Domain-Driven Design's tactical patterns, is a **cluster of one or more entities and value objects** that are always loaded, modified, and persisted together as a single unit, because together they must satisfy a set of business rules — **invariants** — that cannot be verified by looking at any one object in isolation. Every aggregate designates exactly one of its member entities as the **aggregate root**: the sole object that code outside the aggregate is permitted to reference directly. Everything else inside the aggregate is private to it and reached only by first going through the root: - child entities - value objects - collections Think of an `Order` aggregate: the `Order` entity is the root, and `OrderLine` is a child entity that only exists, and only makes sense, in the context of a specific order. ## The rule, and the shortcut it closes The rule "reference other aggregates by ID, not by object reference" is the mechanism that makes the encapsulation above actually hold at the level of the whole application, not just inside one class. If an `OrderConfirmed` handler kept a live in-memory pointer to a `Customer` aggregate root, any code with access to that `Order` could reach `order.customer.loyaltyPoints -= 50` directly, mutating `Customer` state without going through `Customer`'s own root methods, its invariant checks, or its own transaction. The invariant that "loyalty points never go negative" would then depend on every caller behaving correctly rather than being enforced in one place. Holding a `CustomerId` value object instead — a plain identifier, often just a wrapped UUID or long — makes that shortcut structurally impossible: you cannot call a method on an ID, you can only ask a repository to load the `Customer` aggregate fresh, go through its root, and let it apply its own rules. ## Why the rule exists More broadly, DDD treats each aggregate as an independent consistency boundary and, in most implementations, an independent unit of persistence and locking (frequently one row or one root table plus its owned child rows, loaded and saved as a whole via one repository). If aggregates held direct references to each other, the object graph in memory would blur those boundaries: 1. loading one aggregate could transitively pull in an unbounded chain of others; 2. saving one could accidentally cascade writes into another's table; 3. two aggregates could end up needing to be locked and committed together, which defeats the purpose of splitting them at all. ID references keep the graph shallow: an `Order` aggregate stores `CustomerId`, not a `Customer`, so loading an `Order` never implicitly loads a `Customer`, and modifying an `Order` never risks writing to the customer table in the same transaction. ## The trade-off The trade-off is **indirection cost**. With object references, reading `order.customer.name` is a single in-memory hop; with ID references, displaying an order confirmation screen that needs the customer's name requires either: - a second repository call to load the `Customer` aggregate, or - more commonly in production systems, a read-side query/projection that joins the data outside the aggregate model entirely (a CQRS-style read model or a simple SQL join for display purposes only, never for mutating). Teams new to DDD often find this awkward and are tempted to "just" keep a reference for convenience. The failure mode this produces is **invariant leakage** — validation logic that should live inside one aggregate's root methods gets duplicated or half-implemented in whatever service happens to hold both references, and over time nobody can say with confidence which code path is allowed to change customer state. ## A second, subtler failure mode A second, subtler failure mode shows up in persistence frameworks like JPA/Hibernate: mapping an aggregate association as a lazy or eager @ManyToOne/@OneToMany object reference instead of storing a foreign-key-typed ID field silently reintroduces the object-graph problem the ID rule was meant to prevent — a save on the "outer" aggregate can cascade and flush changes into the "inner" one if cascade types aren't scoped carefully, corrupting the intended transaction boundary. This is why many DDD-aligned codebases deliberately map ID references as plain scalar/value-object columns rather than JPA associations, even though it costs an extra query when the related name or details are needed for display. ## Where it shows up A concrete, widely cited example is the classic e-commerce Order/Customer/Product split described in Eric Evans's original DDD book and popularized further by Vaughn Vernon's "Effective Aggregate Design" articles: - Order references CustomerId and each OrderLine references a ProductId, never the live Customer or Product objects; - precisely so that placing an order never risks corrupting catalog pricing or customer account state in the same transaction; - and so each aggregate can evolve, scale, and be sharded independently.

  • If you need to display a customer's name on an order confirmation page, and Order only stores CustomerId, how do you get that name without breaking the aggregate rule?
    You don't reach through the Order aggregate for it — you either issue a separate query to a Customer read model/repository, or build a dedicated read-side projection (a view or denormalized table) that joins order and customer data for display purposes only. The rule against object references applies to the domain/write model where invariants are enforced; read-only display paths are free to join across aggregates because they never mutate anything.
  • What goes wrong if you map an aggregate's reference to another aggregate as a JPA @ManyToOne with CascadeType.ALL instead of storing a plain ID?
    Saving the referencing aggregate can cascade and persist or delete changes on the referenced aggregate in the same transaction, which silently merges two supposedly independent consistency boundaries and can corrupt or delete data the referenced aggregate's own root never sanctioned. It also usually pulls the related aggregate into memory eagerly or via a lazy proxy, reintroducing the tight coupling ID references were meant to avoid.
  • Does 'one aggregate per transaction' mean an application-level use case can never touch two aggregates in the same request?
    No — a single use case can load, modify, and save two different aggregates in the same request, but conventionally each aggregate gets its own transaction/save, and if the second save fails after the first succeeded, the system is momentarily inconsistent and that gap is closed asynchronously (e.g. via a domain event or a saga), not by wrapping both saves in one database transaction.

Like giving someone your employee ID badge number instead of a master key to your office — they can ask to see you through the front desk (repository), but can't let themselves into your files directly.

saying these in an interview costs you the question

  • says entities inside an aggregate can be reached directly from outside
  • keeps live object references between aggregates for convenience
  • maps aggregate associations as eager JPA relations with cascade-all
  • conflates 'reference by ID' with 'never join for reads'
  • thinks one transaction can safely span multiple aggregates as the default design

context

open as a page

In a layered application built with Domain-Driven Design, what is the basic difference between an 'application service' and a 'domain service'?

level: juniorimportance: must knowfreq 70%

basics

~20 s

An application service coordinates a use case — starts transactions, checks permissions, calls objects, converts data — without deciding business rules. A domain service holds business rules that don't fit one entity, like a rule spanning two accounts.

open as a page

In Domain-Driven Design, what is a domain event, and why is it conventionally named in the past tense, like 'OrderShipped' rather than 'ShipOrder'?

level: juniorimportance: must knowfreq 70%

basics

~20 s

A domain event is a note that something meaningful already happened in the business, e.g. 'OrderShipped'. It's named in the past tense because it records a fact that occurred, not a request asking for something to happen.

open as a page

In Domain-Driven Design, what is a Domain Service, and what's a simple example of when you'd need one instead of putting a method on an entity or value object?

level: juniorimportance: must knowfreq 70%

basics

~20 s

A Domain Service holds business logic that doesn't fit on one specific object. You use one when an operation needs several objects together, or is really an action rather than a 'thing' - like transferring money between two accounts.

open as a page

In Domain-Driven Design, what is the core difference between an Entity and a Value Object, and why does that difference change how you implement equality checks (equals/hashCode) for each?

level: juniorimportance: must knowfreq 80%

basics

~20 s

An Entity is something with its own identity that stays the same over time even if its details change — like a person has one ID for life. A Value Object has no identity of its own; it's just described by its data, so two Value Objects with the same data are treated as equal and interchangeable.

open as a page

In Domain-Driven Design, why might a team introduce a dedicated factory method or factory class to create an aggregate instead of just calling `new Order(...)` directly wherever an order is needed?

level: juniorimportance: must knowfreq 55%

basics

~20 s

Because creating some objects takes several steps and rules that must all be checked together; a factory does that work in one place so every part of the code creates the object correctly and never ends up half-built or invalid.

open as a page

In Domain-Driven Design, what is a Repository, and why is it often described as giving client code the illusion of an in-memory collection of objects?

level: juniorimportance: must knowfreq 82%

basics

~20 s

A repository is a piece of code that lets the rest of the app save and fetch domain objects (like 'add this order' or 'find order #5') without knowing or caring whether they're stored in a database, a file, or memory - it just feels like working with a list.

open as a page

In Domain-Driven Design, what is the Specification pattern, and what problem does it solve compared to scattering `if` conditions across your codebase?

level: juniorimportance: must knowfreq 55%

basics

~20 s

A Specification is an object that holds a business rule ("is this order overdue?") and can answer yes/no for any candidate. Instead of copy-pasting the same if-check in five places, you write it once as an object and reuse it everywhere.

open as a page

In Domain-Driven Design, what is an 'intention-revealing interface' for a class or method, and why does a name like isOverdue() help more than a name like checkFlag() when other developers need to keep changing the code?

level: juniorimportance: must knowfreq 65%

basics

~20 s

A name (of a class, method, or interface) should tell you what it does and why, not how it's built inside, so you can use it correctly without reading its guts. isOverdue() says exactly what you get back; checkFlag() forces you to go read the code.

open as a page

In Domain-Driven Design, when designing an aggregate's boundary, what heuristics determine how many entities/value objects it should include, and why do most experienced DDD practitioners favor small aggregates?

level: middleimportance: must knowfreq 75%

basics

~20 s

Include only what must change together, at the same instant, to keep one business rule true. Keep aggregates small: fewer things bundled together means fewer collisions when different requests try to change the same aggregate at once.

open as a page

In a DDD-layered backend, an application service method is often described as 'transaction, security, orchestration, DTO mapping — but no business rules.' What does each of those four responsibilities concretely mean in code, and why must business rules stay out of this layer?

level: middleimportance: must knowfreq 75%

basics

~20 s

An application service starts the transaction, checks the caller is authorized, calls domain objects in order, and converts results to an API shape — but never decides business rules; those live in the domain objects it calls.

open as a page

Concretely, how does an aggregate raise a domain event without breaking encapsulation - for example, without directly calling out to an event bus from inside its business logic?

level: middleimportance: must knowfreq 65%

basics

~20 s

The aggregate keeps a private list inside itself. When its business method changes state, it also adds an event object to that list. Some outside code (like an application service) reads the list after the method returns and publishes the events - the aggregate itself never talks to any bus.

open as a page

When designing a Domain Service, how should the ubiquitous language guide its name and method signatures, and what's the tell-tale sign that you've actually built a technical helper class wearing a domain costume?

level: middleimportance: must knowfreq 65%

basics

~20 s

Name the service after the business action it performs, using words business people use - not generic tech words like Manager or Helper. If you can't explain the name in one sentence, it's not a real domain concept.

open as a page

An unsaved Entity (say, a new `Order` object) doesn't yet have a database-assigned id — it's null until the first save. If equals() is implemented purely by comparing id, what breaks when two such transient, not-yet-persisted Order instances are compared or placed in a hash-based collection, and how do teams typically handle Entity equality before an id exists?

level: middleimportance: must knowfreq 65%

basics

~20 s

If equals() only checks the id and the id is null before saving, then two different unsaved orders both look 'equal' or the equality logic becomes unreliable, which can cause objects to get lost in Sets/Maps once the id is assigned. Teams usually fall back to comparing object references until an id exists, or generate the id in code (like a UUID) before saving so it's never null.

open as a page

When designing a factory for an aggregate, how do you decide between putting a static/companion factory method directly on the aggregate root versus writing a separate, standalone Factory class?

level: middleimportance: must knowfreq 45%

basics

~20 s

Use a method on the object itself when creation only needs data already going into that object; use a separate factory class when creation needs outside help, like other aggregates, ID generators, or several related objects built together.

open as a page

When an application loads an existing aggregate back from a database row or an event stream, why is this 'reconstitution' path usually kept separate from the factory method used to create a brand-new aggregate, even though both end up producing the same aggregate type?

level: middleimportance: must knowfreq 50%

basics

~20 s

Making something new has to check business rules like 'is this order allowed to exist yet,' but loading something that already existed and was already checked once shouldn't re-run those creation-time checks - it should just rebuild the object from stored data.

open as a page

In DDD, why is the rule 'one repository per aggregate root' - not one per entity, not one per database table - and what breaks when a team violates it?

level: middleimportance: must knowfreq 78%

basics

~20 s

You only build a repository for the 'root' object of a cluster of related objects (like an Order, not its individual OrderLine items), because that root is the only safe entry point - going around it to touch the inner pieces directly can leave data in a broken, inconsistent state.

open as a page

What does 'persistence ignorance' mean for a domain model accessed through repositories, and what specific repository design choices support it or quietly undermine it?

level: middleimportance: must knowfreq 74%

basics

~20 s

Persistence ignorance means your core business objects (like an Order) don't know or care how they're saved - no database annotations, no SQL, no ORM-specific base classes in the domain code itself. The repository is where that knowledge lives instead, kept out of the model.

open as a page

How do composable specifications work - what does it mean to combine two Specification objects with AND, OR, and NOT, and why is that composability valuable?

level: middleimportance: must knowfreq 60%

basics

~20 s

You can glue simple rules together like Lego: "expensive" AND "overdue" makes a new rule that's true only when both are true. Each combined rule is itself a Specification, so you can keep combining without ever rewriting the original checks.

open as a page

In Domain-Driven Design's Supple Design, what is the practical difference between a 'command' and a 'side-effect-free function' (query), and why is it worth deliberately separating them even though a single method could do both more 'efficiently'?

level: middleimportance: must knowfreq 55%

basics

~20 s

A query just answers a question and changes nothing; a command changes something and doesn't need to return an answer. Keeping them separate means you can call a query as many times as you like without worrying it will break anything.

open as a page

In Domain-Driven Design, concretely, what mechanism does an aggregate root use to guarantee that its invariants are never violated, from the moment a command comes in to the moment the change is persisted?

level: seniorimportance: must knowfreq 70%

basics

~20 s

Only the aggregate root's own methods can change its data — no direct field edits. Each method checks the rules first, then the whole change saves as one atomic write, so nothing half-valid is ever stored.

open as a page

A codebase's OrderApplicationService.placeOrder method has grown over two years to include inventory checks, tax calculation, fraud scoring, and shipping rule logic, all as inline conditionals, while the Order entity itself only has getters and setters. What is this anti-pattern called, why does it happen incrementally, and how would you refactor it?

level: seniorimportance: must knowfreq 65%

basics

~20 s

This is the anemic domain model anti-pattern — entities hold no behavior, logic piles into services. It happens because adding one more 'if' is easier than redesigning the entity. Fix: move each rule to the object owning its data.

open as a page

When an aggregate raises a domain event and an in-process listener reacts to it by modifying a different aggregate, should that listener's change happen in the same database transaction as the original aggregate's change? What are the consistency implications either way?

level: seniorimportance: must knowfreq 60%

basics

~20 s

You can make the listener run in the same transaction so both aggregates change together, or let it run after the transaction commits so they change separately. Same-transaction means both always agree, but couples them tightly. After-commit means they're looser, but for a moment one might be out of date.

open as a page

Consider transferring money between two Account aggregates, where withdrawing from a source account and depositing into a target account must jointly enforce a rule like 'the source must have sufficient funds.' Walk through why this is best modeled as a Domain Service rather than as a method directly on the Account entity, and what trade-offs that choice introduces.

level: seniorimportance: must knowfreq 60%

basics

~20 s

Each Account should only protect its own balance rule, like not going negative. A transfer touches two accounts, so a separate FundsTransferService coordinates withdraw on one and deposit on the other, rather than one account controlling another.

open as a page

How do you decide whether to model a concept — say, a customer's shipping Address — as an Entity or a Value Object, and why might the same real-world concept be modeled differently in two different bounded contexts of the same system?

level: seniorimportance: must knowfreq 70%

basics

~30 s

Ask: does the business need to track this specific instance over time and tell it apart from an identical-looking one, or does it just describe a value that's interchangeable if the details match? An address used just to ship a package is usually a Value Object (only the street/city/zip matters). But if a 'Verified Address' needs its own approval history and audit trail, it becomes an Entity with its own identity.

open as a page

A factory for an Account aggregate needs to enforce that the account's opening balance is never negative AND that its owner's email is not already used by another account. What's the difference in how a factory should handle these two invariants, and why does that difference matter?

level: seniorimportance: must knowfreq 40%

basics

~20 s

The balance check only needs the data you're already given, so the factory can check it instantly by itself. The email-uniqueness check needs to look at other accounts somewhere else, so the factory has to ask something outside itself - and that check can go stale between asking and actually saving.

open as a page

When a Specification's isSatisfiedBy logic needs to run as a database query instead of filtering an in-memory list, what's the core translation challenge, and how do teams typically solve it without duplicating the rule?

level: seniorimportance: must knowfreq 65%

basics

~20 s

Checking one object in code and asking a database "give me all matching rows" are different jobs. If you write the rule twice - once as code, once as SQL - they can drift apart. Teams solve this by having the specification generate the query itself instead of writing it separately.

open as a page

In Domain-Driven Design, 'conceptual contours' means shaping classes and methods to follow the natural divisions in the domain rather than an engineer's convenience. Given a ShippingCalculator whose single calculate(order, address, weight, isExpress, hasInsurance, discountCode) method has grown unwieldy, how would you use conceptual contours to decide how to split it?

level: seniorimportance: must knowfreq 45%

basics

~20 s

Instead of splitting the method by lines of code or by what's easy to test, split it along the real concepts the business already thinks in, like 'base rate' vs 'express surcharge' vs 'insurance fee', because those are the pieces that actually change independently over time.

open as a page

How should unit-testing strategy differ between a domain service (like a pricing calculator that applies business rules) and an application service (like a use-case orchestrator that opens a transaction and calls repositories) in a DDD-layered backend?

level: middleimportance: should knowfreq 55%

basics

~20 s

A domain service can be tested with plain objects — call it and check the result. An application service usually needs mocked or fake repositories to test, since its job is coordinating things, not doing the business math itself.

open as a page

Why are Domain Services in DDD expected to be stateless, and what specifically breaks in production if a team adds mutable instance fields to one?

level: middleimportance: should knowfreq 55%

basics

~20 s

A Domain Service shouldn't remember anything between calls - each call gets everything it needs as input. If it stores data in its own fields instead, two concurrent calls can mix up or overwrite each other's data.

open as a page

showing 1–30 of 53