Last active
April 8, 2024 12:40
-
-
Save akhansari/b43a9a60ba1ca3f2a8c8705aa0db3efb to your computer and use it in GitHub Desktop.
TypeScript prototype of the Decider pattern. (F# version: https://github.com/akhansari/EsBankAccount)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as assert from "assert"; | |
/** Decider Pattern **/ | |
type Transaction = { | |
amount: number | |
date: Date | |
} | |
type Deposited = Transaction & { | |
kind: "deposited" | |
} | |
type Withdrawn = Transaction & { | |
kind: "withdrawn" | |
} | |
type Closed = { | |
kind: "closed" | |
closedOn: Date | |
} | |
export type Event_ = | |
| Deposited | |
| Withdrawn | |
| Closed | |
type State = { | |
Balance: number | |
IsClosed: boolean | |
} | |
export module State { | |
export const initial: Readonly<State> = { | |
Balance: 0, | |
IsClosed: false, | |
} | |
} | |
export const evolve = (state: Readonly<State>, event: Readonly<Event_>): Readonly<State> => { | |
switch (event.kind) { | |
case "deposited": | |
return { ...state, Balance: state.Balance + event.amount } | |
case "withdrawn": | |
return { ...state, Balance: state.Balance - event.amount } | |
case "closed": | |
return { ...state, IsClosed: true } | |
} | |
} | |
export type Command = | |
| { kind: "deposit", amount: number, date: Date } | |
| { kind: "withdraw", amount: number, date: Date } | |
| { kind: "close", date: Date } | |
export module Command { | |
export const deposit = (amount: number, date: Date): Command => | |
({ kind: "deposit", amount: amount, date: date }) | |
export const withdraw = (amount: number, date: Date): Command => | |
({ kind: "withdraw", amount: amount, date: date }) | |
export const close = (date: Date): Command => | |
({ kind: "close", date: date }) | |
} | |
export const decide = (command: Readonly<Command>) => (state: Readonly<State>): ReadonlyArray<Event_> => { | |
switch (command.kind) { | |
case "deposit": | |
return [ { kind: "deposited", amount: command.amount, date: command.date } ] | |
case "withdraw": | |
return [ { kind: "withdrawn", amount: command.amount, date: command.date } ] | |
case "close": | |
const events: Event_[] = [] | |
if (state.Balance > 0) { | |
events.push({ kind: "withdrawn", amount: state.Balance, date: command.date }) | |
} | |
events.push({ kind: "closed", closedOn: command.date }) | |
return events | |
} | |
} | |
/** Tests **/ | |
function deciderSpec<S, C, E>( | |
initialState: Readonly<S>, | |
evolve: (s: Readonly<S>, e: Readonly<E>) => Readonly<S>, | |
decide: (c: Readonly<C>) => (s: Readonly<S>) => ReadonlyArray<E>) { | |
const spec: { State: Readonly<S>, Outcome: ReadonlyArray<E> } = { | |
State: initialState, | |
Outcome: [], | |
} | |
return { | |
given(events: ReadonlyArray<E>) { | |
spec.State = events.reduce(evolve, spec.State) | |
return this | |
}, | |
when(command: Readonly<C>) { | |
const events = decide(command)(spec.State) | |
spec.State = events.reduce(evolve, spec.State) | |
spec.Outcome = events | |
return this | |
}, | |
then(expectedEvents: ReadonlyArray<E>) { | |
assert.deepStrictEqual(spec.Outcome, expectedEvents) | |
return this | |
}, | |
} | |
} | |
const test = (message: string, fn: Function): void => { | |
try { | |
fn() | |
console.log(" ✅ " + message) | |
} catch (e) { | |
console.error(" ❌ " + message) | |
console.error(e) | |
} | |
} | |
const anyDate = new Date(2000, 1, 1) | |
test("make a deposit", () => { | |
deciderSpec(State.initial, evolve, decide) | |
.when(Command.deposit(5, anyDate)) | |
.then([ { kind: "deposited", amount: 5, date: anyDate } ]) | |
}) | |
test("make a withdrawal", () => { | |
deciderSpec(State.initial, evolve, decide) | |
.when(Command.withdraw(5, anyDate)) | |
.then([ { kind: "withdrawn", amount: 5, date: anyDate } ]) | |
}) | |
test("close the account and withdraw the remaining amount", () => { | |
deciderSpec(State.initial, evolve, decide) | |
.given([ | |
{ kind: "deposited", amount: 50, date: anyDate }, | |
]) | |
.when(Command.deposit(50, anyDate)) | |
.when(Command.close(anyDate)) | |
.then([ | |
{ kind: "withdrawn", amount: 100, date: anyDate }, | |
{ kind: "closed", closedOn: anyDate }, | |
]) | |
}) |
@cjjohansen It's a complicated question, because this bank account example is oversimplified.
I'm not an expert of banking system, but for sure we are in a fully asynchronous system.
Normally we do a request and once the remote system acknowledge the transfer then we can make a withdrawal.
It makes sens to have a MoneyTransferRequest stream to persist different steps and especially all legal events.
So there shouldn't be any change in the BankAccount stream.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
How would you model a money transfer from one account to another. It must be some kind of processor that assures that withdrawal and then deposit succeeds. With possible compensating action? Would you create a new desider maybe named money transfer with say initiate, withdraw, deposit, complete commands and corresponding events? Also it would have to subscribe to events from the account aggregates.