0

I am designing a toy project dealing with the bank account management in F# with Event Sourcing.

I would like to get a bit of review (not really a code review) about my Commands and Events to see if things make sense to someone else.

// Force to use a certain Guid generation function
type BankAccountId = BankAccountId of Guid

// Force to have positive values via a constructor function
type Money = private Money of decimal


type PersonalName = {
    FirstName: string;
    MiddleInitial: string option;
    LastName: string;
}

type AccountInitialization = {
    Owner: PersonalName 
}

type CashDeposit = {
    AccountId: BankAccountId
    Amount: Money
}

type CashWithdrawal = {
    AccountId: BankAccountId
    Amount: Money
}

type Command =
    | InitializeAccount of AccountInitialization
    | WireMoney of AccountTransaction
    | DepositCash of CashDeposit
    | WithdrawCash of CashWithdrawal

type AccountInitialized = {
    Owner: PersonalName
    AccountId: BankAccountId
}

type AccountDebited = {
    Amount: Money
    From: BankAccountId
    To: BankAccountId
}

type AccountCredited = {
    Amount: Money
    From: BankAccountId
    To: BankAccountId
}

type CashDeposited = {
    AccountId: BankAccountId
    Amount: Money
}

type CashWithdrawn = {
    AccountId: BankAccountId
    Amount: Money
}

type InvalidOperationAttempted = {
    AccountId: BankAccountId

}

type Event =
    | AccountInitialized of AccountInitialized
    | AccountCredited of AccountCredited 
    | AccountDebited of AccountDebited 
    | CashDeposited of CashDeposited
    | CashWithdrawn of CashWithdrawn
    | InvalidOperationAttempted 

First, in this current design, seems there is a lot of redundant information accross different types, I am not sure whether in F# this is a common practice to have a single type for different kind of Event / Command data that are "sort of" related:

For examples about those events below:

type CashWithdrawn = {
    AccountId: BankAccountId
    Amount: Money
}

type CashDeposited = {
    AccountId: BankAccountId
    Amount: Money
}

and the commands below:

type CashDeposit = {
    AccountId: BankAccountId
    Amount: Money
}

type CashWithdrawal = {
    AccountId: BankAccountId
    Amount: Money
}

Not sure is it better to have something like that for commands:

type CashOperation = {
    AccountId: BankAccountId
    Amount: Money
}

and like this for events;

type CashOperationOccured = {
    AccountId: BankAccountId
    Amount: Money
}

or even worse / better?:

type CashOperation = {
    AccountId: BankAccountId
    Amount: Money
}

for both commands and events.

Pushing that logic a bit forward I could also pretend that wiring money and cash operations are almost the same in the sense that From To could be the same bank account, but that sounds a bit too twisted to me.

Also not sure that having two different events (ie. AccountDebited AccountCredited) for one command (ie. WireMoney) is smart, considering that I am storing events, the projection might be double the amount of money at some point. However it can seem legit to have those two events. I am a bit confused.

  • 4
    The standard way in double-entry accounting would be to have a single command CreateJournalEntry. Every command always involves two accounts, and always involves credits and debits in equal amounts overall. A cash deposit or withdrawal involves the relevant person's account and a special cash account which accounts for the total amount of cash on hand. – Steve Jun 3 at 11:00
  • I dislike that the label AccountId is associated with the type BankAccountId everywhere. – king-side-slide Jun 3 at 14:59
  • @king-side-slide why? – Ehouarn Perret Jun 3 at 15:11
  • For the same reason (using a bit of hyperbole here) I would dislike using the label Cat associated with the type Dog. It is only in your mind that AccountId and BankAccountId are even remotely related. I had to do a double-take and scroll back up to make sure I didn't miss something when I reached that the first time. – king-side-slide Jun 3 at 15:21
  • @king-side-slide what would you suggest then, just Id: Guid? – Ehouarn Perret Jun 3 at 15:29
1

Personally, I do think that the main goal of DDD is to make code communicate the same language as domain experts do thus easing the mental burden of mapping actual business domain to your code.

That's why I think that having both DepositCash and WithdrawCash is better as it more closely resembles the language of domain experts.

Regarding the notion of DRY I think that duplication here is not that critical as we just duplicate properties, not some business rules. Also, abstracting away both DepositCash and WithdrawCash behind some single abstraction and imposing a dynamic polymorphism is indeed more OOP thing.

P.S. As a bonus here's a nice demo of how DDD+ES+CQRS is already done.

1

Adding to the existing comments and answers, I would also suggest that you can use interfaces to abstract semantically equivalent concepts from different concrete types. For example, you may have DepositCash and WithdrawCash commands, both of which implement IJournalEntryCommand, and then have a single command processor called createJournalEntry which takes IJournalEntryCommand as its parameter.

type IJournalEntryCommand =
    abstract member CreditAccount: AccountId
    abstract member DebitAccount: AccountId
    abstract member CreditAmount: Money
    abstract member DebitAmount: Money

type DepoistCash =
    {
        Account: AccountId
        Amount: Money
    } interface IJournalEntryCommand =
          member this.CreditAccount = this.Account
          member this.DebitAccount = SpecialAccounts.Cash
          member this.CreditAmount = this.Amount
          member this.DebitAmount = this.Amount |> Money.negate

type WithdrawCash =
    {
        Account: AccountId
        Amount: Money
    } interface IJournalEntryCommand =
          member this.CreditAccount = SpecialAccounts.Cash
          member this.DebitAccount = this.Account
          member this.CreditAmount = this.Amount |> Money.negate
          member this.DebitAmount = this.Amount

module JournalCommandProcessor =
    let createJournalEntry (command: IJournalEntryCommand) =
        ...

Then, on the events, you could follow the same pattern and have CashDeposited and CashWithdrawn which both implement IJournalEntryEvent.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.