Skip to content

Instantly share code, notes, and snippets.

@orodio
Last active May 28, 2021 06:30
Show Gist options
  • Save orodio/bacfd83d7c0a152e56cd10a717e3024b to your computer and use it in GitHub Desktop.
Save orodio/bacfd83d7c0a152e56cd10a717e3024b to your computer and use it in GitHub Desktop.
Event Sourcing

Wat

  1. Commands vs Events
  2. Complex Addition

Commands.

Something that CAN happen.

IT'S A COMMAND If you answer yes to any of these.

  • It is future tense.
  • It can fail.

Events.

Something that DID happen.

IT'S AN EVENT If you answer yes to any of these.

  • It is past tense.
  • It can't fail.

Event Sourced Systems.

  • 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.

Addition.

  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)

Complex Addition

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)

Complexer Addition

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
    }
  }

Complexest Addition

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

    }
  }

End result is your database table

Events

  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!
  }

Graph

 
  type User {
    userId: ID!
    accounts: [Account]!
    name: String
  }

  type AccountType {
    accountTypeId: ID!
    label: String
  }

  type Account {
    accountId: ID!
    accountType: AccountType!
    user: User!
  }

User Projection Service

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)

AccountType Projection Service

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)

Account Projection Service

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)

Assumptions in what we have talked about

  • Events are coming in order
  • We are getting all of the events

There are many ways of getting around the above.

Event Storming

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?

Next Time

An event storming session

We are going to Event Storm the MVP functionality for Twitter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment