Build a Banking System
Complete event-sourced banking with commands, projections, and real-time updates
Build a production-ready banking system with DeltaBase. You’ll learn the patterns used in real applications:
- Commands & Events with the Decider pattern
- Projections for fast account balance lookups
- Webhook subscriptions for event delivery
- Real-time updates via WebSocket
┌─────────────────────────────────────────────────────────────────────────┐│ Banking System Architecture ││ ││ HTTP API Event Store Read Models ││ ┌──────────┐ ┌───────────┐ ┌──────────────┐ ││ │ POST │──Command──▶ │ account- │──Webhook─▶│ balances │ ││ │ /deposit │ │ {id} │ │ transactions │ ││ └──────────┘ └───────────┘ └──────────────┘ ││ │ ▲ ││ │ WebSocket │ ││ ▼ │ ││ ┌──────────┐ │ ││ │ Browser │◀────Query────────┘ ││ └──────────┘ │└─────────────────────────────────────────────────────────────────────────┘Time: ~45 minutes
What You’ll Build
Section titled “What You’ll Build”By the end of this tutorial, you’ll have:
- Bank accounts with deposits, withdrawals, and transfers
- Business rules that prevent overdrafts and invalid operations
- Fast balance lookups via projected read models
- Transaction history as a queryable projection
- Real-time balance updates via WebSocket (optional)
Prerequisites
Section titled “Prerequisites”- Node.js 18+
- DeltaBase CLI installed
- Basic TypeScript knowledge
Part 1: Project Setup
Section titled “Part 1: Project Setup”Start DeltaBase Locally
Section titled “Start DeltaBase Locally”npx @delta-base/cli devpnpx @delta-base/cli devbunx @delta-base/cli devYou should see:
Deltabase development environment is ready!
API Server: http://localhost:8787Studio UI: http://localhost:3000Keep this running throughout the tutorial.
Create Your Project
Section titled “Create Your Project”In a new terminal:
mkdir banking-systemcd banking-systemnpm init -ynpm install @delta-base/server @delta-base/toolkit hononpm install -D typescript @types/node tsxmkdir banking-systemcd banking-systempnpm initpnpm add @delta-base/server @delta-base/toolkit honopnpm add -D typescript @types/node tsxmkdir banking-systemcd banking-systembun init -ybun add @delta-base/server @delta-base/toolkit honobun add -D typescript @types/node tsxCreate tsconfig.json:
{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "outDir": "dist" }, "include": ["src"]}Project Structure
Section titled “Project Structure”banking-system/├── src/│ ├── core/│ │ └── accounts.ts # Domain logic (Decider)│ ├── functions/│ │ ├── deposit.ts # Command handlers│ │ ├── withdraw.ts│ │ └── open-account.ts│ ├── projections/│ │ ├── balances.projection.ts│ │ ├── balances.handler.ts│ │ └── balances.route.ts│ ├── shared/│ │ └── event-store.ts # DeltaBase client│ └── index.ts # HTTP server├── deltabase.config.ts # Infrastructure config├── package.json└── tsconfig.jsonPart 2: Domain Modeling
Section titled “Part 2: Domain Modeling”Define Events
Section titled “Define Events”Events are facts - things that happened. They’re immutable.
Create src/core/accounts.ts:
import type { Command, Decider, Event, ReadEvent } from '@delta-base/toolkit';import { IllegalStateError, ValidationError } from '@delta-base/toolkit';
export namespace Accounts { // ═══════════════════════════════════════════════════════════════════ // EVENTS - Facts that happened (immutable, past tense) // ═══════════════════════════════════════════════════════════════════
export type AccountOpened = Event< 'account.opened', { accountId: string; customerId: string; initialBalance: number; openedAt: string; } >;
export type MoneyDeposited = Event< 'money.deposited', { accountId: string; amount: number; description?: string; depositedAt: string; } >;
export type MoneyWithdrawn = Event< 'money.withdrawn', { accountId: string; amount: number; description?: string; withdrawnAt: string; } >;
export type AccountClosed = Event< 'account.closed', { accountId: string; reason?: string; closedAt: string; } >;
export type AccountEvents = | AccountOpened | MoneyDeposited | MoneyWithdrawn | AccountClosed;
// ═══════════════════════════════════════════════════════════════════ // COMMANDS - Intent (can be rejected) // ═══════════════════════════════════════════════════════════════════
export type OpenAccount = Command< 'account.open', { accountId: string; customerId: string; initialBalance: number; } >;
export type DepositMoney = Command< 'money.deposit', { accountId: string; amount: number; description?: string; } >;
export type WithdrawMoney = Command< 'money.withdraw', { accountId: string; amount: number; description?: string; } >;
export type CloseAccount = Command< 'account.close', { accountId: string; reason?: string; } >;
export type AccountCommands = | OpenAccount | DepositMoney | WithdrawMoney | CloseAccount;
// ═══════════════════════════════════════════════════════════════════ // STATE - Current state of an account (derived from events) // ═══════════════════════════════════════════════════════════════════
export interface AccountState { accountId?: string; customerId?: string; balance: number; isOpen: boolean; openedAt?: string; closedAt?: string; }
// ═══════════════════════════════════════════════════════════════════ // DECIDER - Business logic as pure functions // ═══════════════════════════════════════════════════════════════════
/** * decide: Given current state and a command, return events (or throw) * This is where business rules live. */ export const decide = ( command: AccountCommands, state: AccountState ): AccountEvents | AccountEvents[] => { switch (command.type) { case 'account.open': return handleOpenAccount(command, state); case 'money.deposit': return handleDeposit(command, state); case 'money.withdraw': return handleWithdraw(command, state); case 'account.close': return handleCloseAccount(command, state); } };
const handleOpenAccount = ( command: OpenAccount, state: AccountState ): AccountOpened => { // Business rule: Can't open an account that already exists if (state.isOpen) { throw new IllegalStateError( `Account ${command.data.accountId} already exists`, { accountId: command.data.accountId }, 'open', 'open account' ); }
// Business rule: Initial balance must be non-negative if (command.data.initialBalance < 0) { throw new ValidationError('Initial balance must be non-negative', { initialBalance: command.data.initialBalance, }); }
return { type: 'account.opened', data: { accountId: command.data.accountId, customerId: command.data.customerId, initialBalance: command.data.initialBalance, openedAt: new Date().toISOString(), }, }; };
const handleDeposit = ( command: DepositMoney, state: AccountState ): MoneyDeposited => { // Business rule: Account must exist if (!state.isOpen) { throw new IllegalStateError( `Account ${command.data.accountId} does not exist or is closed`, { accountId: command.data.accountId }, state.closedAt ? 'closed' : 'not_found', 'deposit money' ); }
// Business rule: Deposit amount must be positive if (command.data.amount <= 0) { throw new ValidationError('Deposit amount must be positive', { amount: command.data.amount, }); }
return { type: 'money.deposited', data: { accountId: command.data.accountId, amount: command.data.amount, description: command.data.description, depositedAt: new Date().toISOString(), }, }; };
const handleWithdraw = ( command: WithdrawMoney, state: AccountState ): MoneyWithdrawn => { // Business rule: Account must exist if (!state.isOpen) { throw new IllegalStateError( `Account ${command.data.accountId} does not exist or is closed`, { accountId: command.data.accountId }, state.closedAt ? 'closed' : 'not_found', 'withdraw money' ); }
// Business rule: Withdrawal amount must be positive if (command.data.amount <= 0) { throw new ValidationError('Withdrawal amount must be positive', { amount: command.data.amount, }); }
// Business rule: No overdrafts allowed if (state.balance < command.data.amount) { throw new ValidationError( `Insufficient funds. Balance: $${state.balance}, Requested: $${command.data.amount}`, { balance: state.balance, requested: command.data.amount, } ); }
return { type: 'money.withdrawn', data: { accountId: command.data.accountId, amount: command.data.amount, description: command.data.description, withdrawnAt: new Date().toISOString(), }, }; };
const handleCloseAccount = ( command: CloseAccount, state: AccountState ): AccountClosed => { if (!state.isOpen) { throw new IllegalStateError( `Account ${command.data.accountId} does not exist or is already closed`, { accountId: command.data.accountId }, state.closedAt ? 'closed' : 'not_found', 'close account' ); }
// Business rule: Must have zero balance to close if (state.balance !== 0) { throw new ValidationError( `Cannot close account with non-zero balance: $${state.balance}`, { balance: state.balance } ); }
return { type: 'account.closed', data: { accountId: command.data.accountId, reason: command.data.reason, closedAt: new Date().toISOString(), }, }; };
/** * evolve: Given current state and an event, return new state * This is how we rebuild state from events. */ export const evolve = ( state: AccountState, event: ReadEvent<AccountEvents> ): AccountState => { switch (event.type) { case 'account.opened': return { accountId: event.data.accountId, customerId: event.data.customerId, balance: event.data.initialBalance, isOpen: true, openedAt: event.data.openedAt, };
case 'money.deposited': return { ...state, balance: state.balance + event.data.amount, };
case 'money.withdrawn': return { ...state, balance: state.balance - event.data.amount, };
case 'account.closed': return { ...state, isOpen: false, closedAt: event.data.closedAt, }; } };
/** * initialState: Starting state before any events */ export const initialState = (): AccountState => ({ balance: 0, isOpen: false, });
/** * The complete Decider - used by handleCommandWithDecider */ export const decider: Decider<AccountState, AccountCommands, AccountEvents> = { decide, evolve, initialState, };}Key insight: The Decider pattern separates:
decide- Business rules (when can this happen?)evolve- State transitions (how does state change?)initialState- Starting point
All three are pure functions - easy to test, easy to reason about.
Part 3: Command Handlers
Section titled “Part 3: Command Handlers”Command handlers connect HTTP requests to the domain logic.
Shared Event Store Client
Section titled “Shared Event Store Client”Create src/shared/event-store.ts:
import { DeltaBase } from '@delta-base/server';
// For local developmentconst deltabase = new DeltaBase({ baseUrl: process.env.DELTABASE_BASE_URL || 'http://localhost:8787', apiKey: process.env.DELTABASE_API_KEY,});
export const eventStore = deltabase.getEventStore( process.env.DELTABASE_EVENT_STORE_NAME || 'banking');Open Account Handler
Section titled “Open Account Handler”Create src/functions/open-account.ts:
import { handleCommandWithDecider } from '@delta-base/toolkit';import { Accounts } from '../core/accounts';import { eventStore } from '../shared/event-store';
export interface OpenAccountRequest { customerId: string; initialBalance?: number;}
export interface OpenAccountResult { accountId: string; balance: number; message: string;}
export async function openAccountHandler( request: OpenAccountRequest): Promise<OpenAccountResult> { // Generate a unique account ID const accountId = `account-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const command: Accounts.OpenAccount = { type: 'account.open', data: { accountId, customerId: request.customerId, initialBalance: request.initialBalance ?? 0, }, };
const result = await handleCommandWithDecider( eventStore, accountId, // Stream ID = Account ID command, Accounts.decider );
return { accountId, balance: result.newState.balance, message: 'Account opened successfully', };}Deposit Handler
Section titled “Deposit Handler”Create src/functions/deposit.ts:
import { handleCommandWithDecider } from '@delta-base/toolkit';import { Accounts } from '../core/accounts';import { eventStore } from '../shared/event-store';
export interface DepositRequest { amount: number; description?: string;}
export interface DepositResult { newBalance: number; message: string;}
export async function depositHandler( accountId: string, request: DepositRequest): Promise<DepositResult> { const command: Accounts.DepositMoney = { type: 'money.deposit', data: { accountId, amount: request.amount, description: request.description, }, };
const result = await handleCommandWithDecider( eventStore, accountId, command, Accounts.decider );
return { newBalance: result.newState.balance, message: `Deposited $${request.amount}`, };}Withdraw Handler
Section titled “Withdraw Handler”Create src/functions/withdraw.ts:
import { handleCommandWithDecider } from '@delta-base/toolkit';import { Accounts } from '../core/accounts';import { eventStore } from '../shared/event-store';
export interface WithdrawRequest { amount: number; description?: string;}
export interface WithdrawResult { newBalance: number; message: string;}
export async function withdrawHandler( accountId: string, request: WithdrawRequest): Promise<WithdrawResult> { const command: Accounts.WithdrawMoney = { type: 'money.withdraw', data: { accountId, amount: request.amount, description: request.description, }, };
const result = await handleCommandWithDecider( eventStore, accountId, command, Accounts.decider );
return { newBalance: result.newState.balance, message: `Withdrew $${request.amount}`, };}Part 4: Projections
Section titled “Part 4: Projections”Projections build read models optimized for queries. Instead of replaying events every time, we maintain pre-computed views.
Balance Read Model
Section titled “Balance Read Model”Create src/projections/balances.projection.ts:
import type { EventTypeOf, IReadModelStore, Projection, ReadEvent,} from '@delta-base/toolkit';import { Accounts } from '../core/accounts';
// The shape of our read modelexport interface AccountBalanceReadModel { accountId: string; customerId: string; balance: number; isOpen: boolean; openedAt: string; closedAt?: string; lastTransactionAt: string; _revision: number; // For idempotency}
export class AccountBalancesProjection implements Projection<Accounts.AccountEvents>{ private readonly prefix = 'account-balance';
readonly supportedEventTypes: EventTypeOf<Accounts.AccountEvents>[] = [ 'account.opened', 'money.deposited', 'money.withdrawn', 'account.closed', ];
constructor(private store: IReadModelStore) {}
async processEvents( events: ReadEvent<Accounts.AccountEvents>[] ): Promise<void> { for (const event of events) { if (this.supportedEventTypes.includes(event.type)) { await this.project(event); } } }
private async project( event: ReadEvent<Accounts.AccountEvents> ): Promise<void> { const key = `${this.prefix}:${event.streamId}`; const existing = await this.store.get<AccountBalanceReadModel>(key);
// Idempotency check if (existing && existing._revision >= event.streamPosition) { return; }
switch (event.type) { case 'account.opened': await this.handleAccountOpened(event, key); break; case 'money.deposited': await this.handleMoneyDeposited(event, key, existing); break; case 'money.withdrawn': await this.handleMoneyWithdrawn(event, key, existing); break; case 'account.closed': await this.handleAccountClosed(event, key, existing); break; } }
private async handleAccountOpened( event: ReadEvent<Accounts.AccountOpened>, key: string ): Promise<void> { const readModel: AccountBalanceReadModel = { accountId: event.data.accountId, customerId: event.data.customerId, balance: event.data.initialBalance, isOpen: true, openedAt: event.data.openedAt, lastTransactionAt: event.data.openedAt, _revision: event.streamPosition, };
await this.store.put(key, readModel); }
private async handleMoneyDeposited( event: ReadEvent<Accounts.MoneyDeposited>, key: string, existing: AccountBalanceReadModel | null ): Promise<void> { if (!existing) { console.error(`Account ${event.streamId} not found for deposit`); return; }
const updated: AccountBalanceReadModel = { ...existing, balance: existing.balance + event.data.amount, lastTransactionAt: event.data.depositedAt, _revision: event.streamPosition, };
await this.store.put(key, updated); }
private async handleMoneyWithdrawn( event: ReadEvent<Accounts.MoneyWithdrawn>, key: string, existing: AccountBalanceReadModel | null ): Promise<void> { if (!existing) { console.error(`Account ${event.streamId} not found for withdrawal`); return; }
const updated: AccountBalanceReadModel = { ...existing, balance: existing.balance - event.data.amount, lastTransactionAt: event.data.withdrawnAt, _revision: event.streamPosition, };
await this.store.put(key, updated); }
private async handleAccountClosed( event: ReadEvent<Accounts.AccountClosed>, key: string, existing: AccountBalanceReadModel | null ): Promise<void> { if (!existing) { return; }
const updated: AccountBalanceReadModel = { ...existing, isOpen: false, closedAt: event.data.closedAt, _revision: event.streamPosition, };
await this.store.put(key, updated); }}Projection Handler
Section titled “Projection Handler”Create src/projections/balances.handler.ts:
import type { IReadModelStore, ReadEvent } from '@delta-base/toolkit';import { Accounts } from '../core/accounts';import { AccountBalancesProjection } from './balances.projection';
export interface ProcessEventsRequest { events: ReadEvent<Accounts.AccountEvents>[];}
export interface ProcessEventsResult { processedEventCount: number;}
export async function processBalanceEventsHandler( store: IReadModelStore, request: ProcessEventsRequest): Promise<ProcessEventsResult> { const projection = new AccountBalancesProjection(store);
await projection.processEvents(request.events);
return { processedEventCount: request.events.length, };}Projection Webhook Route
Section titled “Projection Webhook Route”Create src/projections/balances.route.ts:
import { Hono } from 'hono';import { processBalanceEventsHandler } from './balances.handler';import type { IReadModelStore } from '@delta-base/toolkit';
export function createBalancesProjectionRoute(store: IReadModelStore) { const app = new Hono();
app.post('/events', async (c) => { // Verify projection auth token const authHeader = c.req.header('Authorization'); const expectedToken = process.env.PROJECTION_AUTH_TOKEN || 'dev-token';
if (authHeader !== `Bearer ${expectedToken}`) { return c.json({ error: 'Unauthorized' }, 401); }
const body = await c.req.json(); const { events } = body;
if (!events || !Array.isArray(events)) { return c.json({ error: 'Invalid request body' }, 400); }
const result = await processBalanceEventsHandler(store, { events });
return c.json({ success: true, ...result, }); });
return app;}Part 5: HTTP Server
Section titled “Part 5: HTTP Server”Bring everything together in src/index.ts:
import { Hono } from 'hono';import { cors } from 'hono/cors';import { serve } from '@hono/node-server';import { InMemoryReadModelStore } from '@delta-base/toolkit';
import { openAccountHandler } from './functions/open-account';import { depositHandler } from './functions/deposit';import { withdrawHandler } from './functions/withdraw';import { createBalancesProjectionRoute } from './projections/balances.route';import type { AccountBalanceReadModel } from './projections/balances.projection';
const app = new Hono();
app.use('*', cors());
// Read model store (use KV in production)const readModelStore = new InMemoryReadModelStore();
// ═══════════════════════════════════════════════════════════════════// COMMAND ENDPOINTS (write side)// ═══════════════════════════════════════════════════════════════════
app.post('/accounts', async (c) => { try { const body = await c.req.json(); const result = await openAccountHandler({ customerId: body.customerId, initialBalance: body.initialBalance, }); return c.json(result, 201); } catch (error: any) { return c.json({ error: error.message }, 400); }});
app.post('/accounts/:accountId/deposit', async (c) => { try { const accountId = c.req.param('accountId'); const body = await c.req.json(); const result = await depositHandler(accountId, { amount: body.amount, description: body.description, }); return c.json(result); } catch (error: any) { return c.json({ error: error.message }, 400); }});
app.post('/accounts/:accountId/withdraw', async (c) => { try { const accountId = c.req.param('accountId'); const body = await c.req.json(); const result = await withdrawHandler(accountId, { amount: body.amount, description: body.description, }); return c.json(result); } catch (error: any) { return c.json({ error: error.message }, 400); }});
// ═══════════════════════════════════════════════════════════════════// QUERY ENDPOINTS (read side - from projections)// ═══════════════════════════════════════════════════════════════════
app.get('/accounts/:accountId', async (c) => { const accountId = c.req.param('accountId'); const account = await readModelStore.get<AccountBalanceReadModel>( `account-balance:${accountId}` );
if (!account) { return c.json({ error: 'Account not found' }, 404); }
return c.json(account);});
app.get('/accounts', async (c) => { const accounts = await readModelStore.list<AccountBalanceReadModel>( 'account-balance:' ); return c.json(accounts);});
// ═══════════════════════════════════════════════════════════════════// PROJECTION WEBHOOK (receives events from DeltaBase)// ═══════════════════════════════════════════════════════════════════
app.route( '/api/projections/balances', createBalancesProjectionRoute(readModelStore));
// ═══════════════════════════════════════════════════════════════════// START SERVER// ═══════════════════════════════════════════════════════════════════
const port = Number(process.env.PORT) || 3001;
console.log(`Banking API running on http://localhost:${port}`);console.log(`Endpoints: POST /accounts - Open account POST /accounts/:id/deposit - Deposit money POST /accounts/:id/withdraw - Withdraw money GET /accounts/:id - Get account (from projection) GET /accounts - List accounts (from projection) POST /api/projections/balances/events - Webhook for projections`);
serve({ fetch: app.fetch, port });
export default app;Part 6: Infrastructure Configuration
Section titled “Part 6: Infrastructure Configuration”Create deltabase.config.ts to define your event store and webhook subscription:
import type { InfrastructureConfig as DeltaBaseConfig } from '@delta-base/server';
const SERVICE_URL = process.env.SERVICE_URL || 'http://localhost:3001';const EVENT_STORE_NAME = process.env.DELTABASE_EVENT_STORE_NAME || 'banking';const PROJECTION_TOKEN = process.env.PROJECTION_AUTH_TOKEN || 'dev-token';
const config: DeltaBaseConfig = { eventStores: [ { name: EVENT_STORE_NAME, description: 'Banking system events', subscriptions: [ { id: 'account-balances-projection', eventFilter: [ 'account.opened', 'money.deposited', 'money.withdrawn', 'account.closed', ], subscriberType: 'webhook', webhook: { url: `${SERVICE_URL}/api/projections/balances/events`, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${PROJECTION_TOKEN}`, }, retryPolicy: { maxAttempts: 5, backoffMinutes: 2, }, }, }, ], }, ],};
export default config;Part 7: Running the System
Section titled “Part 7: Running the System”1. Deploy Infrastructure
Section titled “1. Deploy Infrastructure”First, create the event store and subscription:
pnpx @delta-base/cli deploy2. Start Your Server
Section titled “2. Start Your Server”npx tsx src/index.tspnpx tsx src/index.tsbun run src/index.ts3. Test the API
Section titled “3. Test the API”# Open an accountcurl -X POST http://localhost:3001/accounts \ -H "Content-Type: application/json" \ -d '{"customerId": "cust-123", "initialBalance": 100}'
# Note the accountId from the response, then:
# Deposit moneycurl -X POST http://localhost:3001/accounts/account-xxx/deposit \ -H "Content-Type: application/json" \ -d '{"amount": 50, "description": "Paycheck"}'
# Try to withdraw more than balance (should fail)curl -X POST http://localhost:3001/accounts/account-xxx/withdraw \ -H "Content-Type: application/json" \ -d '{"amount": 200}'
# Withdraw valid amountcurl -X POST http://localhost:3001/accounts/account-xxx/withdraw \ -H "Content-Type: application/json" \ -d '{"amount": 30, "description": "Coffee"}'
# Get account balance (from projection)curl http://localhost:3001/accounts/account-xxx
# List all accountscurl http://localhost:3001/accounts4. Check the Studio
Section titled “4. Check the Studio”Open http://localhost:3000 to see:
- Your events in real-time
- Stream details for each account
- Event payloads and metadata
Part 8: Real-time Balance Updates (Optional)
Section titled “Part 8: Real-time Balance Updates (Optional)”Add WebSocket support to receive live balance updates in the browser.
Add to src/index.ts:
// Add WebSocket endpoint for real-time updatesapp.get('/ws/accounts/:accountId', async (c) => { const accountId = c.req.param('accountId');
// Connect to DeltaBase WebSocket const ws = new WebSocket( `ws://localhost:8787/event-stores/banking/events/ws` );
ws.onopen = () => { ws.send(JSON.stringify({ action: 'subscribe', eventFilter: ['money.deposited', 'money.withdrawn'], streamId: accountId, position: 'latest', })); };
// Forward events to client // (Implementation depends on your WebSocket library)
return c.text('WebSocket endpoint - connect with a WebSocket client');});For a complete WebSocket implementation, see the Real-Time Events Guide.
What You Built
Section titled “What You Built”┌─────────────────────────────────────────────────────────────────────────┐│ Complete Banking Architecture ││ ││ Client ││ │ ││ ├─POST /accounts─────────┐ ││ ├─POST /deposit──────────┤ ││ └─POST /withdraw─────────┤ ││ ▼ ││ ┌─────────────────┐ ││ │ Command Handler │ ││ │ (Decider) │ ││ └────────┬────────┘ ││ │ ││ ▼ ││ ┌─────────────────┐ ││ │ Event Store │ ││ │ (DeltaBase) │ ││ └────────┬────────┘ ││ │ ││ Webhook ││ │ ││ ▼ ││ ┌─────────────────┐ ┌─────────────────┐ ││ │ Projection │─────▶│ Read Model │ ││ │ (Balances) │ │ (KV Store) │ ││ └─────────────────┘ └────────┬────────┘ ││ │ ││ Client │ ││ │ │ ││ ├─GET /accounts/:id──────────────────────────────┘ ││ └─GET /accounts──────────────────────────────────┘ │└─────────────────────────────────────────────────────────────────────────┘You now have:
- Domain logic isolated in the Decider (pure functions, testable)
- Command handlers that use
handleCommandWithDeciderfor atomic operations - Projections that build read models via webhooks
- Fast queries from projected read models
- Infrastructure as Code with
deltabase.config.ts
What’s Next?
Section titled “What’s Next?”- CQRS Implementation Guide - Deeper dive into the architecture
- Real-Time Events - Add WebSocket connections
- Deploy to Production - Go live
- Shopping Cart Tutorial - Multi-aggregate example with inventory