Skip to content

Instantly share code, notes, and snippets.

@debasishg
Last active October 26, 2023 23:11
Show Gist options
  • Save debasishg/a78f3c716173e0dfe142d4ce238e3c7e to your computer and use it in GitHub Desktop.
Save debasishg/a78f3c716173e0dfe142d4ce238e3c7e to your computer and use it in GitHub Desktop.
a brief intro to algebraic domain modeling

Algebraic Domain Modeling

When we talk about the algebra of an operation, we talk about the signature of the operation, and how, starting from the input, we can just "follow the types" to lead to the implementation of that operation.

Consider the following operation generateTrades as part of a domain service TradingService. The idea is to generate all trades that happened on the day (input to the operation) and executed by the user (input to the operation) in the back-office of a securities trading organization.

In the example below, note the following 2 principles that form the core of the algebraic modeling:

  1. The sequence of steps mentioned in the specification of the method as comments and correspond one-to-one to the types of the operations used in implementing generateTrades. I have annotated the steps in the implementation below.
  2. The implementation of generateTrades is completely decoupled from the implementation of the operations like queryExecutionsForDate, getAccountNoFromExecution etc. which we use here. Just follow the types of these operations and compose using the appropriate combinators.

This is the essence of algebraic domain modeling. The implementation evolves from the specification through proper composition of types that model the domain.

trait TradingService:
  /** Generate trades for the day and by a specific user. Here are the steps:
    *
    *  1. Query all executions for the day 
    *  2. Group executions by order no 
    *  3. For each order no, get the account no from the order details 
    *  4. Allocate the trade to the client account 
    *  5. Store the trade
    */
  def generateTrades(
      date: LocalDate,
      userId: UserId
  ): ZStream[Any, Throwable, Trade] =
    queryExecutionsForDate(date)                         // 1
      .groupByKey(_.orderNo):                            // 2
        case (orderNo, executions) =>
          executions
            .via(getAccountNoFromExecution)              // 3
            .via(allocateTradeToClientAccount(userId))   // 4
            .via(storeTrades)                            // 5

  /** Get client account numbers from a stream of executions
    */
  def getAccountNoFromExecution: ZPipeline[Any, Throwable, Execution, (Execution, AccountNo)]

  /** Generate trades from executions and allocate to the associated client account
    */
  def allocateTradeToClientAccount(userId: UserId): ZPipeline[Any, Throwable, (Execution, AccountNo), Trade]

  /** persist trades to database and return the stored trades
    */
  def storeTrades: ZPipeline[Any, Throwable, Trade, Trade]

  /** query all exeutions for the day
    */
  def queryExecutionsForDate(date: LocalDate): ZStream[Any, Throwable, Execution]

For a complete implementation of algebraic domain modeling techniques, take a look at https://github.com/debasishg/tradeio3/tree/main.

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