Introduction
"Why was this order cancelled?" — with normal CRUD, only the final state remains. Event Sourcing stores all change history as events, allowing you to reconstruct state at any point in time. Generate designs with Claude Code.
CLAUDE.md Event Sourcing Rules
## Event Sourcing Design Rules
### Event Design
- Name events in past tense (OrderPlaced, OrderCancelled)
- Events are immutable (append-only)
- Include aggregateId, version, occurredAt in every event
- Payload is JSON (schema versioning required)
### Aggregates
- Derive state by replaying events (never mutate state directly)
- Optimistic locking with version number (conflict detection)
- 1 aggregate = 1 transaction (don't update multiple aggregates simultaneously)
### Projections (Read Model)
- Build read data asynchronously from events
- Projections can be deleted and rebuilt (events are the truth)
- Separate write and read models with CQRS
Generated Event Sourcing Implementation
// src/eventSourcing/eventStore.ts
export class EventStore {
async append(aggregateId: string, aggregateType: string, events: OrderEvent[], expectedVersion: number): Promise<void> {
await prisma.$transaction(async (tx) => {
const currentVersion = await tx.$queryRaw<Array<{ max: number }>>`
SELECT COALESCE(MAX(version), 0) as max FROM events
WHERE aggregate_id = ${aggregateId} FOR UPDATE
`;
if (currentVersion[0].max !== expectedVersion) {
throw new OptimisticLockError(`Expected version ${expectedVersion}, got ${currentVersion[0].max}`);
}
let version = expectedVersion;
for (const event of events) {
version++;
await tx.event.create({
data: { aggregateId, aggregateType, version, type: event.type, data: event.data, occurredAt: new Date() },
});
}
});
}
async getEvents(aggregateId: string): Promise<StoredEvent[]> {
return prisma.event.findMany({ where: { aggregateId }, orderBy: { version: 'asc' } });
}
}
// src/eventSourcing/orderAggregate.ts
export class OrderAggregate {
static fromEvents(aggregateId: string, events: StoredEvent[]): OrderAggregate {
const state = events.reduce((acc, event) => OrderAggregate.applyEvent(acc, event), {
id: aggregateId, userId: '', items: [], status: 'pending', version: 0,
});
return new OrderAggregate(state);
}
private static applyEvent(state: OrderState, event: StoredEvent): OrderState {
switch (event.type) {
case 'OrderPlaced': return { ...state, ...event.data, version: event.version };
case 'OrderCancelled': return { ...state, status: 'cancelled', version: event.version };
case 'OrderShipped': return { ...state, status: 'shipped', version: event.version };
default: return state;
}
}
cancel(reason: string, cancelledBy: string): void {
if (this.state.status !== 'pending') throw new BusinessError('Cannot cancel order in current status');
this.pendingEvents.push({ type: 'OrderCancelled', data: { reason, cancelledBy } });
this.state = { ...this.state, status: 'cancelled' };
}
async save(): Promise<void> {
await eventStore.append(this.state.id, 'Order', this.pendingEvents, this.state.version);
this.pendingEvents = [];
}
}
Projection (Read Model)
export async function rebuildOrderProjection(): Promise<void> {
const checkpoint = await prisma.projectionCheckpoint.findUnique({ where: { name: 'order_projection' } });
const events = await prisma.event.findMany({
where: { aggregateType: 'Order', id: { gt: checkpoint?.lastEventId ?? 0 } },
orderBy: { id: 'asc' }, take: 100,
});
for (const event of events) {
if (event.type === 'OrderPlaced') {
await prisma.orderView.create({ data: { id: event.aggregateId, ...event.data } });
} else if (event.type === 'OrderCancelled') {
await prisma.orderView.update({ where: { id: event.aggregateId }, data: { status: 'cancelled' } });
}
}
if (events.length > 0) {
await prisma.projectionCheckpoint.upsert({
where: { name: 'order_projection' },
create: { name: 'order_projection', lastEventId: events.at(-1)!.id },
update: { lastEventId: events.at(-1)!.id },
});
}
}
Summary
Design Event Sourcing with Claude Code:
- CLAUDE.md — past-tense event naming, append-only, aggregate boundaries
- Optimistic locking with version check (prevent concurrent aggregate writes)
- applyEvent rebuilds state from events (can return to any point in time)
- Projections build denormalized read views from events asynchronously
Review Event Sourcing designs with **Code Review Pack (¥980)* using /code-review at prompt-works.jp*
myouga (@myougatheaxo) — Axolotl VTuber.
Top comments (0)