Why DDD Implementation Is Hard
Eric Evans' blue book is one of the most cited texts in software engineering. It's also one of the most commonly misapplied. Teams read it, understand the concepts, agree they should be doing DDD — and then open a blank TypeScript file and stall out.
The theory is clear enough: aggregates enforce invariants, value objects express domain concepts without identity, domain events capture state changes that other parts of the system care about. The implementation is where things get murky. What does an AggregateRoot base class actually look like? How do you collect and dispatch domain events without coupling your domain layer to your infrastructure? Where does the invariant check go — in the constructor, in a factory method, or in each mutating command?
There are no universal answers, but there are established patterns that experienced DDD practitioners converge on. The problem is that writing the scaffolding for each aggregate from scratch — base classes, event collection, factory methods, repository interfaces, value objects — takes long enough that teams skip it. They end up with anemic domain models: plain objects with setters, business logic scattered across service classes, and a domain layer that's DDD in name only.
CrowVault's fullstack MCP server generates opinionated, complete DDD scaffolding. Not UML diagrams or documentation — actual TypeScript you can drop into a project.
Scaffolding Aggregates with CrowVault
The generate_aggregate tool takes an aggregate name, its fields, the commands it handles, and the events it emits — and produces a complete implementation:
curl -s -X POST https://api.crowvault.ai/v1/tools/call \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"server": "fullstack-mcp-enhanced",
"tool": "generate_aggregate",
"args": {
"name": "Order",
"fields": [
"orderId:string",
"customerId:string",
"total:number",
"status:string"
],
"methods": ["place", "cancel", "complete"],
"events": ["OrderPlaced", "OrderCancelled", "OrderCompleted"]
}
}'
The output is a TypeScript class that actually enforces domain rules — not a data bag:
import { AggregateRoot } from '../shared/AggregateRoot';
import { OrderPlaced } from '../events/OrderPlaced';
import { OrderCancelled } from '../events/OrderCancelled';
import { OrderCompleted } from '../events/OrderCompleted';
type OrderStatus = 'pending' | 'confirmed' | 'cancelled' | 'completed';
interface OrderProps {
orderId: string;
customerId: string;
total: number;
status: OrderStatus;
}
export class Order extends AggregateRoot {
private readonly orderId: string;
private readonly customerId: string;
private total: number;
private status: OrderStatus;
private constructor(props: OrderProps) {
super();
this.orderId = props.orderId;
this.customerId = props.customerId;
this.total = props.total;
this.status = props.status;
}
static place(
orderId: string,
customerId: string,
total: number
): Order {
if (total <= 0) {
throw new Error('Order total must be greater than zero');
}
const order = new Order({
orderId,
customerId,
total,
status: 'pending',
});
order.addDomainEvent(
new OrderPlaced({ orderId, customerId, total })
);
return order;
}
cancel(): void {
if (this.status !== 'pending' && this.status !== 'confirmed') {
throw new Error(
`Cannot cancel order in status '${this.status}'`
);
}
this.status = 'cancelled';
this.addDomainEvent(
new OrderCancelled({ orderId: this.orderId })
);
}
complete(): void {
if (this.status !== 'confirmed') {
throw new Error(
`Cannot complete order that is not confirmed (current: '${this.status}')`
);
}
this.status = 'completed';
this.addDomainEvent(
new OrderCompleted({ orderId: this.orderId })
);
}
getId(): string { return this.orderId; }
getStatus(): OrderStatus { return this.status; }
getTotal(): number { return this.total; }
}
Several things to note here. The constructor is private — the only way to create an Order is through the place() factory method, which means you can never have an Order in an invalid initial state. The invariant checks (total must be positive, cancellation requires a valid prior status) are in the command methods where they belong. Domain events are accumulated on the aggregate via addDomainEvent() — a method on AggregateRoot — and dispatched by the application layer after persistence, keeping infrastructure concerns out of the domain.
The generated AggregateRoot base class is also included in the output: it holds the event collection array, exposes pullDomainEvents() for the application layer to drain and dispatch, and defines the abstract getId() method that repositories require.
Value Objects That Enforce Invariants
A domain model that uses primitive strings and numbers everywhere is fragile. When total is just a number, nothing prevents you from passing a customer's age into a pricing calculation. Value objects give domain concepts their own type identity and move validation to the point of construction.
The generate_value_object tool produces value objects with built-in equality and validation:
curl -s -X POST https://api.crowvault.ai/v1/tools/call \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"server": "fullstack-mcp-enhanced",
"tool": "generate_value_object",
"args": {
"name": "Money",
"fields": ["amount:number", "currency:string"],
"validations": ["amount >= 0", "currency is ISO 4217"]
}
}'
The output is an immutable class with structural equality semantics:
const ISO_4217_CURRENCIES = new Set([
'USD', 'EUR', 'GBP', 'CAD', 'AUD', 'JPY', /* ... */
]);
export class Money {
private constructor(
private readonly amount: number,
private readonly currency: string
) {}
static of(amount: number, currency: string): Money {
if (amount < 0) {
throw new Error(`Money amount cannot be negative: ${amount}`);
}
const upper = currency.toUpperCase();
if (!ISO_4217_CURRENCIES.has(upper)) {
throw new Error(`Invalid ISO 4217 currency code: ${currency}`);
}
return new Money(amount, upper);
}
add(other: Money): Money {
this.assertSameCurrency(other);
return new Money(this.amount + other.amount, this.currency);
}
subtract(other: Money): Money {
this.assertSameCurrency(other);
const result = this.amount - other.amount;
if (result < 0) throw new Error('Money cannot be negative after subtraction');
return new Money(result, this.currency);
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
toString(): string {
return `${this.amount.toFixed(2)} ${this.currency}`;
}
private assertSameCurrency(other: Money): void {
if (this.currency !== other.currency) {
throw new Error(
`Currency mismatch: ${this.currency} vs ${other.currency}`
);
}
}
getAmount(): number { return this.amount; }
getCurrency(): string { return this.currency; }
}
Every Money instance is guaranteed valid at construction time. Any operation that would produce an invalid state — negative balance, mismatched currencies — throws immediately. The private constructor enforces construction through the factory method, so there's no path to an invalid Money object. When you use Money instead of number in your Order aggregate, the TypeScript compiler enforces the intent at the call site — you can't accidentally pass a raw number where a Money is expected.
Connecting the Pieces: Repositories and Domain Events
An aggregate and its value objects don't exist in isolation. They need a repository interface to persist and retrieve them, domain event classes that carry the data downstream consumers need, and domain services for logic that spans multiple aggregates.
The generate_repository tool produces a typed repository interface plus an infrastructure-layer implementation for your database of choice. The interface lives in the domain layer and depends on nothing outside the domain. The concrete implementation lives in the infrastructure layer and can be swapped in tests for an in-memory stub.
The generate_domain_event tool produces typed event classes with a consistent shape: an event ID (UUID), an occurred-at timestamp, and a typed payload. Consistent event structure matters when you're routing events to multiple consumers — an event bus handler should be able to read the envelope without knowing anything about the specific event type.
For cross-aggregate logic, generate_domain_service scaffolds a service class that takes its aggregate dependencies via constructor injection and contains no infrastructure concerns. Domain services are the right place for logic that involves multiple aggregates but doesn't belong to either one — a common example is a pricing calculation that requires both a Product and a Customer to determine the applicable discount.
For the full bounded context workflow — aggregates, value objects, repositories, events, application-layer use cases, and infrastructure wiring — see the fullstack project scaffolding guide.
From Theory to Running Code
The gap between understanding DDD and applying it consistently is mostly a tooling problem. When writing an aggregate from scratch costs 45 minutes, teams skip the pattern under deadline pressure. When it costs 2 minutes, the pattern becomes the path of least resistance.
CrowVault generates DDD scaffolding that follows established patterns from the community — private constructors, factory methods, event collection on the aggregate root, structural equality for value objects, domain-layer interfaces for repositories. The output is TypeScript you can read, understand, and modify. It's not a framework that locks you in — it's a starting point that would have taken you an hour to write yourself.
The API reference covers the full set of domain modeling tools: generate_aggregate, generate_value_object, generate_entity, generate_domain_event, generate_repository, generate_domain_service, generate_factory, and create_bounded_context for scaffolding an entire bounded context in a single call. Get an API key and scaffold your first aggregate in under five minutes.