Skip to content

Part 8: Change a deployed model

The bank is deployed and its accounts have events. A view’s state is derived from those events, so changing a view means computing it again. In this part you change the summary, add a total across every account, and follow what each deploy does with the history.

src/AccountSummary.view.ts
import * as Domain from "@alchemy.run/domain";
import * as Effect from "effect/Effect";
import * as Schema from "effect/Schema";
import { Account } from "./Account.aggregate.ts";
import { CurrentActor } from "./CurrentActor.context.ts";
import { NotOwner } from "./NotOwner.rejection.ts";
import { Session } from "./Session.middleware.ts";
export class AccountSummary extends Domain.View("AccountSummary", {
from: [Account],
key: Account,
middleware: [Session],
rejects: [NotOwner],
state: Schema.Struct({ owner: Schema.NullOr(Schema.String), balance: Schema.Int }),
initial: { owner: null, balance: 0 },
state: Schema.Struct({
owner: Schema.NullOr(Schema.String),
balance: Schema.Int,
movements: Schema.Int,
}),
initial: { owner: null, balance: 0, movements: 0 },
evolve: {
AccountOpened: (v, e) => ({ ...v, owner: e.owner }),
MoneyDeposited: (v, e) => ({ ...v, balance: e.balanceAfter }),
MoneyWithdrawn: (v, e) => ({ ...v, balance: e.balanceAfter }),
MoneyDeposited: (v, e) => ({ ...v, balance: e.balanceAfter, movements: v.movements + 1 }),
MoneyWithdrawn: (v, e) => ({ ...v, balance: e.balanceAfter, movements: v.movements + 1 }),
},
validate: Effect.fn(function* (_request, summary) {
const actor = yield* CurrentActor;
if (summary?.owner && summary.owner !== actor.customerId) return yield* new NotOwner();
}),
}) {}

The summary now counts deposits and withdrawals. Deployed as it is, each stored summary would keep what the old code computed, and only movements from now on would be counted.

src/AccountSummary.view.ts
export class AccountSummary extends Domain.View("AccountSummary", {
from: [Account],
key: Account,
// ...
validate: Effect.fn(function* (_request, summary) {
const actor = yield* CurrentActor;
if (summary?.owner && summary.owner !== actor.customerId) return yield* new NotOwner();
}),
version: 1,
}) {}

A view’s version says it computes something different from what’s stored. Bump it whenever you change a deployed view’s evolve, state, keyOf, or the events and sources it reads, and only ever increase it.

Terminal window
bun alchemy deploy

The deploy uploads the Worker and returns without waiting on the summaries. Each summary is keyed by its account and reads only that account’s events, so it’s rebuilt where it is: it keeps its Durable Object and its storage, and the first time it’s touched after the deploy it reads its account’s events again, with the new code, before it answers.

scripts/try.ts
// Print the balance now and after every change, in the background.
yield* summaries.pipe(
Stream.runForEach((summary) => Effect.log(`balance: ${summary?.balance}`)),
Stream.runForEach((summary) =>
Effect.log(`balance: ${summary?.balance}, movements: ${summary?.movements}`),
),
Effect.forkChild,
);
Terminal window
bun scripts/try.ts https://bank-bankworker-….workers.dev
[12:00:01.527] INFO (#9): balance: 70, movements: 2
[12:00:02.467] INFO (#9): balance: 170, movements: 3
[12:00:03.750] INFO (#9): balance: 140, movements: 4

The first line already counts the deposit and withdrawal you made in Part 5, which happened before movements existed. Your numbers depend on what you’ve done since.

A Durable Object that was busy when you deployed keeps running the code it started with until it goes idle, for up to 5 minutes by default; see Code updates. A summary on the old code can’t rebuild for version 1, so until it moves, the Worker answers its reads itself: it reads the account’s events and computes the summary with the new code, without storing it. Once the object starts on the new code, it rebuilds and answers again.

src/DailyDeposits.view.ts
import * as Domain from "@alchemy.run/domain";
import * as DateTime from "effect/DateTime";
import * as Schema from "effect/Schema";
import { Account } from "./Account.aggregate.ts";
export class DailyDeposits extends Domain.View("DailyDeposits", {
from: [Account],
key: Schema.String,
keyOf: { MoneyDeposited: ({ envelope }) => DateTime.formatIsoDateUtc(envelope.at) },
state: Schema.Struct({ total: Schema.Int }),
initial: { total: 0 },
evolve: { MoneyDeposited: (v, e) => ({ total: v.total + e.amount }) },
}) {}

keyOf sends every account’s deposit to the day it happened, such as 2026-11-02, so one key adds up deposits from many accounts.

src/Bank.model.ts
import * as Domain from "@alchemy.run/domain";
import { Account } from "./Account.aggregate.ts";
import { AccountSummary } from "./AccountSummary.view.ts";
import { Customer } from "./Customer.aggregate.ts";
import { DailyDeposits } from "./DailyDeposits.view.ts";
import {
MainAccountProvisioning,
MainAccountProvisioningLive,
} from "./MainAccountProvisioning.policy.ts";
import { Statement } from "./Statement.view.ts";
export class Bank extends Domain.Model("Bank", {
aggregates: [Account, Customer],
views: [AccountSummary, Statement],
views: [AccountSummary, Statement, DailyDeposits],
policies: [MainAccountProvisioning],
}) {}
export const BankHandlers = MainAccountProvisioningLive;
src/BankApi.api.ts
import * as Domain from "@alchemy.run/domain";
import { Bank } from "./Bank.model.ts";
export class BankApi extends Domain.Api("BankApi", {
model: Bank,
commands: ["RegisterCustomer", "DepositMoney", "WithdrawMoney"],
views: ["AccountSummary"],
views: ["AccountSummary", "DailyDeposits"],
}) {}

The totals declare no validate, so any client of the Api can read them.

Terminal window
bun alchemy deploy
· Backfilling view:DailyDeposits, view:Statement (3s) ...
· Backfilling view:DailyDeposits reading history; view:Statement complete (9s) ...
· Backfilled view:DailyDeposits, view:Statement

This deploy waits. A day’s total doesn’t know which accounts deposited that day, so it can’t fetch its own history the way a summary does. Instead, the deploy walks every account the bank has recorded, reads its events, and sends each deposit to its day, then returns once every total has its whole history. It reports how far each view has got while it works, and names every view it keeps filled, so the statement shows up already complete. Deposits made meanwhile wait their turn and arrive in order after the history.

scripts/try.ts
import * as Domain from "@alchemy.run/domain";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
import * as FetchHttpClient from "effect/http/FetchHttpClient";
import * as Layer from "effect/Layer";
import * as Stream from "effect/Stream";
import { AccountId } from "../src/Account.id.ts";
import { BankApi } from "../src/BankApi.api.ts";
import { CustomerId } from "../src/Customer.id.ts";
import { sessionClient } from "../src/Session.middleware.ts";
const [url, customerId = "c-1"] = process.argv.slice(2);
const accountId = AccountId.make(`${customerId}-main`);
const program = Effect.gen(function* () {
const bank = yield* BankApi.client;
// ...
yield* bank.WithdrawMoney(accountId, { amount: 30 });
yield* Effect.sleep("2 seconds");
const today = DateTime.formatIsoDateUtc(yield* DateTime.now);
const totals = yield* bank.DailyDeposits.get(today);
yield* Effect.log(`deposited today: ${totals?.total}`);
});

Run the script again, and the last line adds up every deposit made today, by every customer, including those from before the totals existed:

[12:00:05.910] INFO (#1): deposited today: 5600
src/DailyDeposits.view.ts
export class DailyDeposits extends Domain.View("DailyDeposits", {
from: [Account],
key: Schema.String,
keyOf: { MoneyDeposited: ({ envelope }) => DateTime.formatIsoDateUtc(envelope.at) },
state: Schema.Struct({ total: Schema.Int }),
initial: { total: 0 },
evolve: { MoneyDeposited: (v, e) => ({ total: v.total + e.amount }) },
state: Schema.Struct({ total: Schema.Int, count: Schema.Int }),
initial: { total: 0, count: 0 },
evolve: { MoneyDeposited: (v, e) => ({ total: v.total + e.amount, count: v.count + 1 }) },
version: 1,
}) {}

The totals now count deposits too, so their version goes up, the same as for the summary.

Terminal window
bun alchemy deploy
· Backfilling view:DailyDeposits#1, view:Statement (3s) ...
· Backfilling view:DailyDeposits#1 reading history; view:Statement complete (11s) ...
· Backfilled view:DailyDeposits#1, view:Statement

This deploy waits again. #1 is the totals’ new version. The totals can’t rebuild where they are, for the same reason they couldn’t fill themselves, so they move to fresh storage and the deploy fills it from every account’s events. The old totals are deleted with this deploy, so until it returns, reads see the new totals filling in. A view built on another view is rebuilt whenever the one it reads is, by whichever path that view takes.