Event sourcing
without the ceremony.
Why Bounda
Your events.
Your logic.
Zero boilerplate.
The setup is on us. Traditional implementations bury your domain under layers of infrastructure: command buses, aggregate repositories, event dispatchers, projection runners. Bounda handles all of that wiring. You write only what matters — handlers, appliers, and projections — the runtime connects them.
Types inferred, never written. Bounda derives TypeScript types directly from your Zod schemas — no manual interfaces, no duplication. Every function signature is fully typed and autocompleted in your editor the moment you define the schema. Change a field and the compiler tells you every place that breaks — across commands, events, projections, and queries — before you ship.
Sagas are first-class citizens. Multi-step business workflows, retries, timeouts, and compensations belong in dedicated sagas and policies — not in ad-hoc state machines buried inside handlers. Bounda gives them a home and a lifecycle.
How it works
Bounda
in a nutshell
// domain/order/commands/place-order.ts
export async function handler({ command, events }: Command.HandlerArgs) {
const { aggregateId, customerId, items } = command.payload;
const total = items.reduce((s, i) => s + i.quantity * i.price, 0);
return events.orderPlaced({aggregateId, customerId, items, total});
} // domain/order/commands/place-order.ts
export async function handler({ command, events }: Command.HandlerArgs) {
const { aggregateId, customerId, items } = command.payload;
const total = items.reduce((s, i) => s + i.quantity * i.price, 0);
return events.orderPlaced({aggregateId, customerId, items, total});
} // domain/order/order-placed.ts
export const payload: Event.PayloadFunction = ({ z }) => {
return z.object({
customerId: z.string(),
items: z.array(z.object({
productId: z.string(),
quantity: z.number(),
price: z.number(),
})),
total: z.number().positive(),
});
};
export function apply({ state, event }: Event.ApplierArgs) {
return { ...state, ...event.payload, status: "placed", placedAt: event.timestamp };
} // domain/order/order-placed.ts
export const payload: Event.PayloadFunction = ({ z }) => {
return z.object({
customerId: z.string(),
items: z.array(z.object({
productId: z.string(),
quantity: z.number(),
price: z.number(),
})),
total: z.number().positive(),
});
};
export function apply({ state, event }: Event.ApplierArgs) {
return { ...state, ...event.payload, status: "placed", placedAt: event.timestamp };
} // read/my-orders/projections/order-placed.ts
export function project({ event, projector }: Projection.HandlerArgs) {
projector.insert({
id: event.aggregateId,
customerId: event.payload.customerId,
status: "placed",
total: event.payload.total,
placedAt: event.timestamp,
});
} // read/my-orders/projections/order-placed.ts
export function project({ event, projector }: Projection.HandlerArgs) {
projector.insert({
id: event.aggregateId,
customerId: event.payload.customerId,
status: "placed",
total: event.payload.total,
placedAt: event.timestamp,
});
} // read/my-orders/views/view.ts
export const fields: View.FieldsFunction = () => ({
id: { type: "string", primaryKey: true },
customerId: { type: "string", index: true },
status: { type: "string", index: true },
total: { type: "number", required: true },
placedAt: { type: "string", required: true },
}); // read/my-orders/views/view.ts
export const fields: View.FieldsFunction = () => ({
id: { type: "string", primaryKey: true },
customerId: { type: "string", index: true },
status: { type: "string", index: true },
total: { type: "number", required: true },
placedAt: { type: "string", required: true },
}); // read/my-orders/queries/get-order.ts
export const payload: Query.PayloadFunction = ({ z }) => {
return z.object({ orderId: z.string().uuid() });
};
export const repository: Query.Repository = ({ orderId, client }) => {
return client.prepare("SELECT * FROM order_summary WHERE id = ?").get(orderId) ?? null;
};
export async function handler({ repositoryData }: Query.HandlerArgs) {
if (!repositoryData) return null;
return { ...repositoryData, items: JSON.parse(repositoryData.items) };
} // read/my-orders/queries/get-order.ts
export const payload: Query.PayloadFunction = ({ z }) => {
return z.object({ orderId: z.string().uuid() });
};
export const repository: Query.Repository = ({ orderId, client }) => {
return client.prepare("SELECT * FROM order_summary WHERE id = ?").get(orderId) ?? null;
};
export async function handler({ repositoryData }: Query.HandlerArgs) {
if (!repositoryData) return null;
return { ...repositoryData, items: JSON.parse(repositoryData.items) };
}