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.
Count every movement
Section titled “Count every movement”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.
Bump the version
Section titled “Bump the version”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.
Deploy the change
Section titled “Deploy the change”bun alchemy deployThe 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.
Print the count
Section titled “Print the count” // 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, );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: 4The 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.
Read during the rollout
Section titled “Read during the rollout”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.
Total deposits by day
Section titled “Total deposits by day”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.
Host the totals
Section titled “Host the totals”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;Expose the totals
Section titled “Expose the totals”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.
Deploy and wait for the history
Section titled “Deploy and wait for the history”bun alchemy deploy· Backfilling view:DailyDeposits, view:Statement (3s) ...· Backfilling view:DailyDeposits reading history; view:Statement complete (9s) ...· Backfilled view:DailyDeposits, view:StatementThis 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.
Read today’s total
Section titled “Read today’s total”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: 5600Change the totals
Section titled “Change the totals”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.
Deploy the rebuild
Section titled “Deploy the rebuild”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:StatementThis 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.