- Commands vs Events
- Complex Addition
Something that CAN happen.
IT'S A COMMAND If you answer yes to any of these.
- It is future tense.
- It can fail.
Something that DID happen.
IT'S AN EVENT If you answer yes to any of these.
- It is past tense.
- It can't fail.
- Things that happened (Event) are stored.
- Current State (Projection) are the events added together.
- Different projections can be created by adding events together in different ways.
function identity (value) {
return value
}
function fold(values = [], current, fn = identity) {
if (values.length <= 0) return current
const [head, ...rest] = values
return fold(rest, fn(current, head), fn)
}
var fn = (a, b) => a + b
fold([1, 2, 3], 0, fn)
// fold([2, 3], 1, fn)
// fold([3], 3, fn)
// fold([], 6, fn)
// 6
JAVASCRIPT HAS FOLD
var fn (a, b) => a + b
[1, 2, 3].reduce(fn, 0)
think about the end state first
Our accumulator needs to be the same "type" or "shape" as our final sum. This following example will add together Withdraw and Deposit events. The sum will be a final balance, which is a number.
const START_BALANCE = 0
const AMOUNT_WITHDRAWN = "AMOUNT_WITHDRAWN"
const AMOUNT_DEPOSITED = "AMOUNT_DEPOSITED"
const EVENTS = [
{ wat: AMOUNT_DEPOSITED, value: 10 },
{ wat: AMOUNT_DEPOSITED, value: 20 },
{ wat: AMOUNT_WITHDRAWN, value: 25 },
{ wat: AMOUNT_DEPOSITED, value: 10 },
{ wat: AMOUNT_WITHDRAWN, value: 25 },
]
function calcBalance (balance, event) {
switch (event.wat) {
case AMOUNT_DEPOSITED:
return balance + event.value
case AMOUNT_WITHDRAWN:
return balance - event.value
default:
return balance
}
}
const CURRENT_BALANCE = EVENTS.reduce(calcBalance, START_BALANCE)
Final result can include more than one subject.
This next example will have more than one account. Our final result will be an object with account ids as keys and balances for values. This means our initial value, will be an object.
You will often find in a naive event sourced system that nouns need to be created.
const INITIAL_BALANCES = {}
const ACCOUNT_CREATED = "ACCOUNT_CREATED"
const ACCOUNT_AMOUNT_WITHDRAWN = "ACCOUNT_AMOUNT_WITHDRAWN"
const ACCOUNT_AMOUNT_DEPOSITED = "ACCOUNT_AMOUNT_DEPOSITED"
const EVENTS = [
{ wat: ACCOUNT_CREATED, accountId: "acct:1" },
{ wat: ACCOUNT_CREATED, accountId: "acct:2" },
{ wat: ACCOUNT_CREATED, accountId: "acct:3" },
{ wat: ACCOUNT_AMOUNT_DEPOSITED, accountId: "acct:1", value: 10 },
{ wat: ACCOUNT_AMOUNT_DEPOSITED, accountId: "acct:2", value: 15 },
{ wat: ACCOUNT_AMOUNT_DEPOSITED, accountId: "acct:3", value: 25 },
{ wat: ACCOUNT_AMOUNT_WITHDRAWN, accountId: "acct:2", value: 10 },
]
const BALANCES = EVENTS.reduce(calcBalances, INITIAL_BALANCES)
assert(BALANCES, {
"acct:1": 10,
"acct:2": 5,
"acct:3": 25,
})
function calcBalances (balances, event) {
switch (event.wat) {
case ACCOUNT_CREATED:
balances[event.accountId] = 0
return balances
case ACCOUNT_AMOUNT_DEPOSITED:
balances[event.accountId] += event.value
return balances
case ACCOUNT_AMOUNT_WITHDRAWN:
balances[event.accountId] -= event.value
return balances
default:
return balances
}
}
const TOTAL_BALANCE = EVENTS.reduce(calcTotalBalance, 0)
assert(TOTAL_BALANCE, 40)
function calcTotalBalance (total, event) {
switch (event.wat) {
case ACCOUNT_AMOUNT_DEPOSITED:
return total + event.value
case ACCOUNT_AMOUNT_WITHDRAWN:
return total - event.value
default:
return total
}
}
const TOTAL_DEPOSITED = EVENTS.reduce(calcTotalDeposited, 0)
assert(TOTAL_DEPOSITED, 50)
function calcTotalDeposited (total, event) {
switch (event.wat) {
case ACCOUNT_AMOUNT_DEPOSITED:
return total + event.value
default:
return total
}
}
const TOTAL_WITHDRAWN = EVENTS.reduce(calcTotalWithdrawn, 0)
assert(TOTAL_WITHDRAWN, 10)
function calcTotalWithdrawn (total, event) {
switch (event.wat) {
case ACCOUNT_AMOUNT_WITHDRAWN:
return total - event.value
default:
return total
}
}
You should be able to derive all state in your application from your events
Example:
// event types
const ACCOUNT_TYPE_CREATED = "ACCOUNT_TYPE_CREATED"
const ACCOUNT_TYPE_UPDATED = "ACCOUNT_TYPE_UPDATED"
const USER_CREATED = "USER_CREATED"
const USER_UPDATED = "USER_UPDATED"
const ACCOUNT_CREATED = "ACCOUNT_CREATED"
const ACCOUNT_AMOUNT_DEPOSITED = "AMOUNT_DEPOSITED"
const ACCOUNT_AMOUNT_WITHDRAWN = "AMOUNT_WITHDRAWN"
// stuff that happened
const EVENTS = [
// External Account Created
{ wat: ACCOUNT_TYPE_CREATED, accountTypeId: "acct-type:0" },
{ wat: ACCOUNT_TYPE_UPDATED, accountTypeId: "acct-type:0", field: "label", value: "External" }
{ wat: USER_CREATED, userId: "user:0" },
{ wat: USER_UPDATED, userId: "user:0", field: "name", value: "External"},
{ wat: ACCOUNT_CREATED, userId: "user:0", accountId: "acct:0", accountTypeId: "account-type:0" },
// Savings account created
{ wat: ACCOUNT_TYPE_CREATED, accountTypeId: "acct-type:1" },
{ wat: ACCOUNT_TYPE_UPDATED, accountTypeId: "acct-type:1", field: "label", value: "Savings"},
// Chequing account created
{ wat: ACCOUNT_TYPE_CREATED, accountTypeId: "acct-type:2" },
{ wat: ACCOUNT_TYPE_UPDATED, accountTypeId: "acct-type:2", field: "label", value: "Chequing" },
// User Bob created
{ wat: USER_CREATED, userId: "user:1" },
{ wat: USER_UPDATED, userId: "user:1", field: "name", value: "bob" },
{ wat: ACCOUNT_CREATED, userId: "user:1", accountId: "acct:1", accountTypeId: "acct-type:1" },
{ wat: ACCOUNT_CREATED, userId: "user:1", accountId: "acct:2", accountTypeId: "acct-type:2" },
// User Pat created
{ wat: USER_CREATED, userId: "user:2" },
{ wat: USER_UPDATED, userId: "user:2", field: "name", value: "pat" },
{ wat: ACCOUNT_CREATED, userId: "user:2", accountId: "acct:3", accountTypeId: "acct-type:1" },
// Transactions (All transfer commands)
{ wat: ACCOUNT_AMOUNT_WITHDRAWN, accountId: "acct:0", amount: 50.0, toAccountId: "acct:1" },
{ wat: ACCOUNT_AMOUNT_DEPOSITED, accountId: "acct:1", amount: 50.0, fromAccountId: "acct:0" },
{ wat: ACCOUNT_AMOUNT_WITHDRAWN, accountId: "acct:0", amount: 15.0, toAccountId: "acct:2" },
{ wat: ACCOUNT_AMOUNT_DEPOSITED, accountId: "acct:2", amount: 15.0, fromAccountId: "acct:0" },
{ wat: ACCOUNT_AMOUNT_WITHDRAWN, accountId: "acct:0", amount: 15.0, toAccountId: "acct:2" },
{ wat: ACCOUNT_AMOUNT_DEPOSITED, accountId: "acct:3", amount: 10.0, fromAccountId: "external" },
{ wat: ACCOUNT_AMOUNT_WITHDRAWN, accountId: "acct:2", amount: 25.0, toAccountId: "acct:0" },
{ wat: ACCOUNT_AMOUNT_DEPOSITED, accountId: "acct:0", amount: 25.0, fromAccountId: "acct:2" },
{ wat: ACCOUNT_AMOUNT_WITHDRAWN, accountId: "acct:1", amount: 10.0, toAccountId: "acct:3" },
{ wat: ACCOUNT_AMOUNT_DEPOSITED, accountId: "acct:3", amount: 10.0, fromAccountId: "acct:1" },
]
const USER_ACCOUNTS = EVENTS.reduce(calcUserAccounts, {})
assert(USER_ACCOUNTS, {
"user:0": {
"acct-type:0": ["acct:0"],
},
"user:1": {
"acct-type:1": ["acct:1"],
"acct-type:2": ["acct:2"],
},
"user:2": {
"acct-type:1": ["acct:3"],
}
})
function calcUserAccounts(accounts, event) {
switch (event.wat) {
// { wat: ACCOUNT_CREATED, userId, accountId, accountTypeId }
case ACCOUNT_CREATED:
accounts[event.userId] = accounts[event.userId] || {}
accounts[event.userId][event.accountTypeId] = accounts[event.userId][event.accountTypeId] || []
accounts[event.userId][event.accountTypeId].push(event.accountId)
return accounts
default:
return accounts
}
}
const ACCOUNT_BALANCES = EVENTS.reduce(calcAccountBalances, {})
assert(ACCOUNT_BALANCES, {
"acct:0": -55,
"acct:1": 40,
"acct:2": -10,
"acct:3": 20,
})
function calcAccountBalances(accounts, event) {
switch (event.wat) {
// { wat: ACCOUNT_CREATED, userId, accountId, accountTypeId }
case ACCOUNT_CREATED:
accounts[event.accountId] = 0
return accounts
// { wat: ACCOUNT_AMOUNT_DEPOSITED, accountId, amount, fromAccountId }
case ACCOUNT_AMOUNT_DEPOSITED:
accounts[event.accountId] += event.amount
return accounts
// { wat: ACCOUNT_AMOUNT_WITHDRAWN, accountId, amount, toAccountId }
case ACCOUNT_AMOUNT_WITHDRAWN:
accounts[event.accountId] -= event.amount
return accounts
}
}
enum EventType {
ACCOUNT_TYPE_CREATED
ACCOUNT_TYPE_UPDATED
USER_CREATED
USER_UPDATED
ACCOUNT_CREATED
ACCOUNT_AMOUNT_DEPOSITED
ACCOUNT_AMOUNT_WITHDRAWN
}
interface Event {
wat: EventType!
}
type AccountTypeCreatedEvent implements Event {
wat: EventType!
accountTypeId: ID!
}
type AccountTypeUpdatedEvent implements Event {
wat: EventTYpe!
accountTypeId: ID!
field: String
value: String
}
type UserCreatedEvent implements Event {
wat: EventType!
userId: ID!
}
type UserUpdatedEvent implements Event {
wat: EventType!
userId: ID!
field: String!
value: String!
}
type AccountCreatedEvent implements Event {
wat: EventType!
userId: ID!
accountId: ID!
accountTypeId: ID!
}
type AccountAmountDepositedEvent implements Event {
wat: EventType!
accountId: ID!
amount: Float!
fromAccountId: ID!
}
type AccountAmountWithdrawnEvent implements Event {
wat: EventType!
accountId: ID!
amount: Float!
toAccountId: ID!
}
type User {
userId: ID!
accounts: [Account]!
name: String
}
type AccountType {
accountTypeId: ID!
label: String
}
type Account {
accountId: ID!
accountType: AccountType!
user: User!
}
const CARE_ABOUT = new Set([UserCreatedEvent, UserUpdatedEvent])
async function projectUsers(event) {
if (event.wat === UserCreatedEvent) {
await psql(`INSERT INTO users (user_id) VALUES ($1);`, [event.userId])
} else if (event.wat === UserUpdatedEvent) {
if (event.field === "name") {
await psql(`UPDATE users SET name=$2 WHERE user_id = $1;`, [event.userId, event.value])
}
}
}
for await (const event of subscribeEvents(CARE_ABOUT)) projectUsers(event)
const CARE_ABOUT = new Set([AccountTypeCreatedEvent, AccountTypeUpdatedEvent])
async function projectAccountTypes(event) {
if (event.wat === AccountTypeCreatedEvent) {
await psql(`INSERT INTO account_types (account_type_id) VALUES ($1);`, [event.accountTypeId])
} else if (event.wat === AccountTypeUpdatedEvent) {
if (event.field === "label") {
await psql(`UPDATE account_types SET label=$2 WHERE account_type_id = $1;`, [event.accountTypeId, event.value])
}
}
}
for await (const event of subscribeEvents(CARE_ABOUT)) projectAccountTypes(event)
const CARE_ABOUT = new Set([AccountCreatedEvent, AccountAmountDepositedEvent, AccountAmountWithdrawnEvent])
async function projectAccounts(event) {
if (event.wat === AccountCreatedEvent) {
await psql(`INSERT INTO accounts (account_id, user_id, account_type_id) VALUES ($1, $2, $3);`, [event.account_id, event.userId, event.accountTypeId])
} else if (event.wat === AccountAmountDepositedEvent) {
const account = await psql(`SELECT balance FROM accounts WHERE account_id = $1;`, [event.account_id])
await psql(`UPDATE accounts SET balance=$2 WHERE account_id = $1`, [event.account_id, account.balance + event.amount])
} else if (event.wat === AccountAmountWithdrawnEvent) {
const account = await psql(`SELECT balance FROM accounts WHERE account_id = $1;`, [event.account_id])
await psql(`UPDATE accounts SET balance=$2 WHERE account_id = $1`, [event.account_id, account.balance - event.amount])
}
}
for await (const event of subscribeEvents(CARE_ABOUT)) projectAccounts(event)
- Events are coming in order
- We are getting all of the events
There are many ways of getting around the above.
Workshopping the events you will need for your application or feature.
In the above example we had 7 Event types. They kept track of:
* The creation of Users, Accounts and Account Types
* The updating of fields for Users and Account Types
* Changes in the balance of a given Account
But a banking system probably needs a lot more events than that. Also the events we have are lacking information to be really robust too. ie: Was it a teller who initiated the transfer?
An event storming session
We are going to Event Storm the MVP functionality for Twitter.