DDD · Architecture
The Factory
Knows Where to Put It
Two codebases, written years apart, generate an Aggregate's ID in two different places. The instinct is to ask which one actually follows Domain-Driven Design. The book itself has a more specific — and more interesting — answer than either.
One codebase generates an Aggregate's identifier inside the Aggregate's own constructor — a plain UUID call, no dependency on anything outside the Domain layer. Another generates the identifier by calling out to a Repository method first, in the Application layer, then hands the already-generated ID into a Factory that builds the Aggregate. Same underlying operation — a UUID v4 with the hyphens stripped, nothing more exotic than that — done in two structurally different places. The natural question is which one is "correct" Domain-Driven Design. That question turns out to have a specific, citable answer, and it isn't the one either codebase's own convention implies.
The Two Answers, Side by Side
The constructor version reads like this — the Aggregate is complete and self-sufficient the moment it exists, ID included:
class Order {
readonly orderId: string
constructor(params: { orderId?: string; ... }) {
this.orderId = params.orderId ?? generateId()
}
}The Factory version routes the same value through an extra hop — Application asks Infrastructure for an ID, then hands it to a Factory:
// application layer
const account = accountFactory.create({
...command,
id: await accountRepository.newId(),
})Both produce the exact same kind of value. Neither touches a database to do it — newId() turns out to be nothing more than uuid.v4() under the hood, wrapped in a Repository method for no reason the code itself explains. On the surface, the constructor version looks leaner, and it's tempting to read the Factory version as a codebase that simply hasn't caught up.
What the Book Actually Says
Eric Evans' Domain-Driven Design (2003) addresses this directly, in the chapter on Factories, under a section asking exactly this question — where does the responsibility for assigning identity belong:
"When the program is assigning an identifier, the Factory is a good place to control it. Although the actual generation of a unique tracking id is typically done by a database 'sequence' or other infrastructure mechanism, the Factory knows what to ask for and where to put it."
That is, close to verbatim, the second codebase's shape: a Factory that doesn't generate the identifier itself, but knows to ask an outside mechanism for one and knows where it goes once it arrives. The book doesn't describe the constructor-does-it-all version as the default at all — the Factory-mediated version is the one it actually walks through.
It would be one thing if the book were ambiguous and either reading were defensible. It isn't ambiguous here — this passage exists specifically to answer "who assigns the identifier," and the answer given is Factory-orchestrated, infrastructure-delegated generation. A codebase that does exactly that isn't behind a convention; it's closer to what was actually written.
Why the Original Answer Pointed at Infrastructure
The reason becomes clear from the rest of the passage: in 2003, "the actual generation of a unique tracking id" meant, in the overwhelming majority of real systems, a database sequence — an auto-incrementing counter the database itself owned and handed out on request. There was no way to get that value without asking the database for it, which meant there was no way to write a self-sufficient constructor the way the first codebase's UUID version does. The Factory's job, as the book frames it, was specifically to hide that infrastructure round-trip from the rest of the domain model — the Factory "knows what to ask for," so nothing else has to.
What Changed the Calculus
A UUID doesn't have this problem. Generating one requires no coordination with anything external — no sequence to increment, no round trip, no shared counter to protect from collisions. The entire reason the book routes identity assignment through a Factory talking to an outside mechanism stops applying the moment the identifier no longer needs an outside mechanism at all. A constructor calling a UUID function directly isn't skipping a step DDD requires; it's a simplification that only became available once the identifier stopped depending on infrastructure to exist.
A design rule written to solve a specific technical constraint often keeps being followed after the constraint itself disappears — not because anyone re-evaluated it and chose to keep it, but because the pattern outlived the reason for it and nobody had occasion to ask why it was there. The Factory-mediated version isn't wrong; it's a faithful implementation of guidance written for a world where identity generation was, definitionally, an infrastructure concern. It just never got the chance to notice that, for this particular kind of identifier, that world had already changed.
Neither Codebase Was Actually Wrong
The Factory-mediated codebase is doing precisely what the foundational text describes for program-assigned identity. The constructor-only codebase is doing something the text never explicitly rules out and that later became simpler to justify, once UUIDs made the infrastructure hop optional rather than required. What looked, at first glance, like one codebase following a rule and the other having drifted from it turns out to be neither — one inherited a pattern built for a constraint that no longer exists, and the other quietly stopped needing it.
Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software (Addison-Wesley, 2003), Chapter 6, "Factories" · backend-service-playbook/docs/architecture/aggregate-id.md — the constructor version · nestjs-rest-cqrs-example/AccountFactory.ts — the Factory-mediated version