Bank Account Example

A complete CQRS application demonstrating aggregations, projections, commands with validation, and multiple read models.

Events

class AccountOpened   { has UInt $.account-id; has Rat $.initial-balance }
class AmountDeposited { has UInt $.account-id; has Rat $.amount }
class AmountWithdrawn { has UInt $.account-id; has Rat $.amount }
class AccountClosed   { has UInt $.account-id }

Aggregation

aggregation BankAccount {
    has UInt $.account-id is projection-id;
    has Rat  $.balance = 0;
    has Bool $.is-open = False;

    method apply(AccountOpened $e) {
        $!balance = $e.initial-balance;
        $!is-open = True;
    }
    method apply(AmountDeposited $e) { $!balance += $e.amount }
    method apply(AmountWithdrawn $e) { $!balance -= $e.amount }
    method apply(AccountClosed $e)   { $!is-open = False }

    method deposit(Rat $amount) is command {
        die "Account is closed" unless $!is-open;
        die "Amount must be positive" if $amount <= 0;
        $.amount-deposited: :$amount;
    }

    method withdraw(Rat $amount) is command {
        die "Account is closed" unless $!is-open;
        die "Insufficient funds: $!balance < $amount" if $!balance < $amount;
        $.amount-withdrawn: :$amount;
    }

    method close() is command {
        die "Account is already closed" unless $!is-open;
        $.account-closed;
    }
}

Projections

# Balance view
projection BalanceView {
    has UInt $.account-id is projection-id;
    has Rat  $.balance = 0;

    method apply(AccountOpened $e)   { $!balance = $e.initial-balance }
    method apply(AmountDeposited $e) { $!balance += $e.amount }
    method apply(AmountWithdrawn $e) { $!balance -= $e.amount }
}

# Transaction history
projection TransactionLog {
    has UInt $.account-id is projection-id;
    has Str  @.transactions;

    method apply(AccountOpened $e) {
        @!transactions.push: "Opened with {$e.initial-balance}";
    }
    method apply(AmountDeposited $e) {
        @!transactions.push: "Deposited {$e.amount}";
    }
    method apply(AmountWithdrawn $e) {
        @!transactions.push: "Withdrew {$e.amount}";
    }
    method apply(AccountClosed $e) {
        @!transactions.push: "Account closed";
    }
}

Usage

use Sourcing;
use Sourcing::Plugin::Memory;
Sourcing::Plugin::Memory.use;

# Open an account
my $account = sourcing BankAccount, :account-id(1);
$account.account-opened: initial-balance => 1000;
$account.^update;

# Make transactions
$account.deposit: 500;
$account.^update;

$account.withdraw: 200;
$account.^update;

# Query projections
my $balance = sourcing BalanceView, :account-id(1);
say "Balance: {$balance.balance}";  # 1300

my $log = sourcing TransactionLog, :account-id(1);
say "Transactions:";
for $log.transactions -> $t {
    say "  $t";
}
# Opened with 1000
# Deposited 500
# Withdrew 200

Key Concepts Demonstrated

Next Steps