Skip to content

Domain.Aggregate reference

Source: packages/domain/src/Aggregate.ts

One entity that decides, such as a bank account: a consistency boundary. It takes a command, checks it against its current state, and records events or returns a rejection. Each instance handles one command at a time, and its events, new state and reply commit together.

import * as Domain from "@alchemy.run/domain";
import * as Schema from "effect/Schema";
export const AccountId = Schema.String.pipe(Schema.brand("AccountId"));
export class MoneyWithdrawn extends Domain.Event("MoneyWithdrawn", {
amount: Schema.Int,
balanceAfter: Schema.Int,
}) {}
export class InsufficientFunds extends Domain.Rejection("InsufficientFunds", {
balance: Schema.Int,
requested: Schema.Int,
}) {}
export class WithdrawMoney extends Domain.Command("WithdrawMoney", {
target: AccountId,
input: { amount: Schema.Int },
events: [MoneyWithdrawn],
rejects: [InsufficientFunds],
reply: { balance: Schema.Int },
}) {}
export class Account extends Domain.Aggregate("Account", {
id: AccountId,
state: Schema.Struct({ balance: Schema.Int }),
initial: { balance: 0 },
commands: [WithdrawMoney],
decide: {
WithdrawMoney: (s, cmd) =>
cmd.amount > s.balance
? new InsufficientFunds({ balance: s.balance, requested: cmd.amount })
: [new MoneyWithdrawn({ amount: cmd.amount, balanceAfter: s.balance - cmd.amount })],
},
evolve: {
MoneyWithdrawn: (_, e) => ({ balance: e.balanceAfter }),
},
reply: {
WithdrawMoney: (s) => ({ balance: s.balance }),
},
}) {}

A handler can be an Effect.fn that reads what the command’s middleware provides.

decide: {
WithdrawMoney: Effect.fn(function* (s, cmd) {
const actor = yield* CurrentActor;
if (actor.customerId !== s.owner) return yield* new NotOwner();
return [new MoneyWithdrawn({ amount: cmd.amount, balanceAfter: s.balance - cmd.amount })];
}),
},

read lets an Api expose the aggregate itself, so a client can get it, watch it, and follow its events. validate judges every read.

read: {
middleware: [Session],
rejects: [NotOwner],
validate: Effect.fn(function* (_request, s) {
const actor = yield* CurrentActor;
if (actor.customerId !== s.owner) return yield* new NotOwner();
}),
},
const accounts = yield* Account;
const { reply } = yield* accounts.send(accountId, new WithdrawMoney({ amount: 30 }));
yield* accounts.schedule(accountId, new SendStatement(), { after: "30 days" });