Scaffold Aggregate
Generate a DDD Aggregate Root with domain events, invariant enforcement, and consistency boundaries.
Triggers
- •"scaffold aggregate"
- •"create aggregate"
- •"new aggregate"
- •"scaffold aggregate root"
Instructions
Step 1: Gather Information
Ask the student for:
- •Aggregate name (e.g.,
Order,ShoppingCart,Inventory) - •Properties / child entities (e.g.,
lineItems: LineItem[],status: OrderStatus) - •Commands it should handle (e.g.,
addItem,removeItem,submit) - •Invariants — rules that must always hold (e.g., "order must have at least one item", "cannot add items to a submitted order")
- •Domain events it should emit (e.g.,
OrderPlaced,ItemAdded)
If the student provides a name only, suggest a typical aggregate structure based on the domain.
Step 2: Determine Output Location
Check the current working directory:
- •If in a
before/directory, create files there - •If in a lesson directory, create in
before/ - •Otherwise, create in the current directory
Step 3: Generate Domain Event Types
Create {aggregate-name}-events.ts:
typescript
// === Domain Events ===
// Events are named in past tense — they describe what happened
interface DomainEvent {
readonly eventType: string;
readonly occurredOn: Date;
readonly aggregateId: string;
}
export interface {Aggregate}Created extends DomainEvent {
readonly eventType: '{Aggregate}Created';
// event-specific data
}
export interface {Command}Executed extends DomainEvent {
readonly eventType: '{Command}Executed';
// event-specific data
}
// Discriminated union of all events for this aggregate
export type {Aggregate}Event =
| {Aggregate}Created
| {Command}Executed;
Step 4: Generate Aggregate File
Create {aggregate-name}.ts:
typescript
import { z } from 'zod';
import type { {Aggregate}Event } from './{aggregate-name}-events';
// === Branded ID Type ===
declare const __brand: unique symbol;
type Brand<T, B extends string> = T & { [__brand]: B };
export type {Aggregate}Id = Brand<string, '{Aggregate}Id'>;
export function create{Aggregate}Id(value: string): {Aggregate}Id {
return value as {Aggregate}Id;
}
// === Result Type ===
type Result<T, E = Error> =
| { success: true; value: T }
| { success: false; error: E };
// === Aggregate Root ===
export class {Aggregate} {
private _domainEvents: {Aggregate}Event[] = [];
private constructor(
private readonly _id: {Aggregate}Id,
// private properties
) {}
// === Factory Method ===
static create(
id: {Aggregate}Id,
props: { /* creation props */ }
): Result<{Aggregate}> {
// Validate creation invariants
const aggregate = new {Aggregate}(id);
// Record creation event
aggregate.addDomainEvent({
eventType: '{Aggregate}Created',
occurredOn: new Date(),
aggregateId: id,
});
return { success: true, value: aggregate };
}
// === Identity ===
get id(): {Aggregate}Id {
return this._id;
}
// === Command Methods ===
// Each method enforces invariants and emits events
/*
doSomething(params): Result<void> {
// 1. Check invariants
if (!this.canDoSomething()) {
return { success: false, error: new Error('Invariant violated: ...') };
}
// 2. Apply state change
this._state = newState;
// 3. Record domain event
this.addDomainEvent({
eventType: 'SomethingDone',
occurredOn: new Date(),
aggregateId: this._id,
});
return { success: true, value: undefined };
}
*/
// === Domain Events ===
get domainEvents(): readonly {Aggregate}Event[] {
return [...this._domainEvents];
}
private addDomainEvent(event: {Aggregate}Event): void {
this._domainEvents.push(event);
}
clearDomainEvents(): void {
this._domainEvents = [];
}
// === Equality ===
equals(other: {Aggregate}): boolean {
return this._id === other._id;
}
}
Step 5: Generate Test File
Create {aggregate-name}.test.ts:
typescript
import { describe, it, expect } from 'vitest';
import { {Aggregate}, create{Aggregate}Id } from './{aggregate-name}';
describe('{Aggregate}', () => {
const validId = create{Aggregate}Id('test-id-1');
describe('create', () => {
it('should create with valid properties', () => {
const result = {Aggregate}.create(validId, { /* valid props */ });
expect(result.success).toBe(true);
if (result.success) {
expect(result.value.id).toBe(validId);
}
});
it('should emit {Aggregate}Created event on creation', () => {
const result = {Aggregate}.create(validId, { /* valid props */ });
if (result.success) {
const events = result.value.domainEvents;
expect(events).toHaveLength(1);
expect(events[0].eventType).toBe('{Aggregate}Created');
}
});
});
describe('commands', () => {
// Test each command method
it('should enforce invariants', () => {
// Create aggregate in a state where command should fail
// Attempt command
// Verify failure with appropriate error
});
it('should emit event on successful command', () => {
// Create aggregate
// Execute command
// Verify event emitted
});
});
describe('domain events', () => {
it('should clear events after clearDomainEvents()', () => {
const result = {Aggregate}.create(validId, { /* props */ });
if (result.success) {
expect(result.value.domainEvents).toHaveLength(1);
result.value.clearDomainEvents();
expect(result.value.domainEvents).toHaveLength(0);
}
});
it('should return a copy of events (not the internal array)', () => {
const result = {Aggregate}.create(validId, { /* props */ });
if (result.success) {
const events1 = result.value.domainEvents;
const events2 = result.value.domainEvents;
expect(events1).not.toBe(events2); // Different array references
expect(events1).toEqual(events2); // Same content
}
});
});
});
Step 6: Explain the Pattern
After generating, briefly explain:
- •Aggregate Root — the entry point for the consistency boundary; all changes go through it
- •Domain Events — record what happened; emitted during command execution, dispatched after persistence
- •Invariant enforcement — every command method checks business rules before applying changes
- •Command → Validate → Apply → Event — the standard pattern for aggregate methods
- •
clearDomainEvents()— called by the repository after events have been dispatched - •Consistency boundary — everything inside the aggregate is kept consistent; cross-aggregate consistency is eventual
Customization Notes
- •Replace placeholder commands with the student's actual business operations
- •Add child entities / value objects as needed
- •Keep aggregates small — if it's growing, consider splitting into smaller aggregates
- •Each command method should validate invariants, mutate state, and emit exactly one event
- •Domain events should be immutable and contain only the data needed by subscribers