From harness-claude
Guides implementing CQRS pattern with TypeScript: separate read/write models, command handlers with Prisma, denormalized read models for complex queries.
How this skill is triggered — by the user, by Claude, or both
Slash command
/harness-claude:microservices-cqrs-patternThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Separate read and write models to optimize query and command performance independently.
Separate read and write models to optimize query and command performance independently.
CQRS without event sourcing (simple model separation):
// Commands — write side
interface CreateOrderCommand {
userId: string;
items: { productId: string; quantity: number }[];
shippingAddress: Address;
}
interface UpdateOrderStatusCommand {
orderId: string;
status: 'confirmed' | 'shipped' | 'delivered' | 'cancelled';
note?: string;
}
// Command handlers — use the normalized write DB
class OrderCommandHandler {
constructor(private readonly db: PrismaClient) {}
async handleCreate(cmd: CreateOrderCommand): Promise<string> {
const prices = await this.db.product.findMany({
where: { id: { in: cmd.items.map((i) => i.productId) } },
select: { id: true, price: true },
});
const priceMap = new Map(prices.map((p) => [p.id, p.price]));
const total = cmd.items.reduce(
(sum, item) => sum + priceMap.get(item.productId)! * item.quantity,
0
);
const order = await this.db.order.create({
data: {
userId: cmd.userId,
status: 'pending',
total,
shippingAddress: cmd.shippingAddress,
items: { create: cmd.items.map((i) => ({ ...i, unitPrice: priceMap.get(i.productId)! })) },
},
});
// Synchronously update read model (or via event)
await this.updateReadModel(order.id);
return order.id;
}
async handleUpdateStatus(cmd: UpdateOrderStatusCommand): Promise<void> {
await this.db.order.update({
where: { id: cmd.orderId },
data: { status: cmd.status },
});
await this.updateReadModel(cmd.orderId);
}
private async updateReadModel(orderId: string): Promise<void> {
// Rebuild the denormalized read model
const order = await this.db.order.findUnique({
where: { id: orderId },
include: { items: { include: { product: true } }, user: true },
});
if (order) {
await this.readDb.orderSummary.upsert({
where: { orderId },
update: buildOrderSummary(order),
create: buildOrderSummary(order),
});
}
}
}
// Queries — read side with denormalized read DB
interface OrderListItem {
orderId: string;
status: string;
customerName: string;
total: number;
itemCount: number;
placedAt: Date;
}
class OrderQueryHandler {
constructor(private readonly readDb: ReadDatabase) {}
async listUserOrders(userId: string, cursor?: string): Promise<OrderListItem[]> {
// Fast query on the denormalized read model — no joins needed
return this.readDb.orderSummary.findMany({
where: { userId },
orderBy: { placedAt: 'desc' },
take: 20,
cursor: cursor ? { id: cursor } : undefined,
select: {
orderId: true,
status: true,
customerName: true,
total: true,
itemCount: true,
placedAt: true,
},
});
}
async getOrderDetail(orderId: string): Promise<OrderDetail | null> {
return this.readDb.orderDetail.findUnique({ where: { orderId } });
}
}
CQRS with event-driven read model sync:
// Write side emits events
class OrderCommandHandler {
async handleCreate(cmd: CreateOrderCommand): Promise<string> {
const order = await this.writeDb.order.create({ data: { ...cmd } });
// Emit integration event — read side reacts
await this.eventBus.publish('order.created', {
orderId: order.id,
userId: order.userId,
items: order.items,
total: order.total,
createdAt: order.createdAt.toISOString(),
});
return order.id;
}
}
// Read side subscription — builds the read model asynchronously
class OrderReadModelProjector {
constructor(private readonly readDb: ReadDatabase) {}
async onOrderCreated(event: OrderCreatedEvent): Promise<void> {
await this.readDb.orderSummary.create({
data: {
orderId: event.orderId,
userId: event.userId,
status: 'pending',
total: event.total,
itemCount: event.items.length,
placedAt: new Date(event.createdAt),
customerName: await this.fetchCustomerName(event.userId), // denormalized
},
});
}
async onOrderStatusUpdated(event: OrderStatusUpdatedEvent): Promise<void> {
await this.readDb.orderSummary.update({
where: { orderId: event.orderId },
data: { status: event.status },
});
}
}
API layer — route to command or query handler:
// Commands → write side
app.post('/orders', async (req, res) => {
const orderId = await commandHandler.handleCreate(req.body);
res.status(201).json({ orderId });
});
app.patch('/orders/:id/status', async (req, res) => {
await commandHandler.handleUpdateStatus({ orderId: req.params.id, ...req.body });
res.status(204).send();
});
// Queries → read side
app.get('/orders', async (req, res) => {
const orders = await queryHandler.listUserOrders(req.user.id, req.query.cursor as string);
res.json(orders);
});
app.get('/orders/:id', async (req, res) => {
const order = await queryHandler.getOrderDetail(req.params.id);
if (!order) {
res.status(404).json({ error: 'Not found' });
return;
}
res.json(order);
});
Eventual consistency: When the read model is updated asynchronously (event-driven), there's a window where reads may be stale. This is acceptable for most use cases. For cases where the caller must immediately see their own write, use synchronous read model updates or direct redirect to the write model for the first read.
Read model per use case: You can have multiple read models from the same write data:
order_summary — list view (lightweight)order_detail — full view with itemsorder_analytics — aggregated for reportingAnti-patterns:
When to start simple: CQRS adds operational complexity. Start with a single model. Introduce CQRS when you can measure that reads are slow because of write model constraints.
microservices.io/patterns/data/cqrs.html
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeGuides implementing CQRS to separate read and write models, optimize query performance, and build event-sourced systems.
Designs and implements CQRS patterns for scalable systems, covering logical separation, separate read models, event-sourced CQRS, and query optimization.
Separate command (write) and query (read) models for complex domains. Use when read/write patterns diverge significantly or when audit/consistency requirements demand immutability.