Skip to content

Instantly share code, notes, and snippets.

@xpepper
Last active January 26, 2025 15:14
Show Gist options
  • Save xpepper/bed195b4e7b6090a52db2bd2db45ab5a to your computer and use it in GitHub Desktop.
Save xpepper/bed195b4e7b6090a52db2bd2db45ab5a to your computer and use it in GitHub Desktop.
Domain Modeling Made Functional - chapter 5 - order and order line

In chapter 5, p.95, Scott writes:

"First, is Order an Entity or a Value Object? Obviously it’s an Entity—the details of the order may change over time, but it’s the same order.

What about an OrderLine, though? If we change the quantity of a particular order line, for example, is it still the same order line? In most designs, it would make sense to say yes, it is still the same order line, even though the quantity or price has changed over time. So OrderLine is an Entity too, with its own identifier.

But now here’s a question: if you change an order line, have you also changed the order that it belongs to?

In this case, it’s clear that the answer is yes: changing a line also changes the entire order. In fact, having immutable data structures makes this unavoidable. If I have an immutable Order containing immutable OrderLines, then just making a copy of one of the order lines does not also make a copy of the Order as well. In order to make a change to an OrderLine contained in an Order, I need to make the change at the level of the Order, not at the level of the OrderLine.

For example, here’s some pseudocode for updating the price of an order line:

  /// We pass in three parameters:
  /// * the top-level order
  /// * the id of the order line we want to change
  /// * the new price
  let changeOrderLinePrice order orderLineId newPrice =
    // 1. find the line to change using the orderLineId
    let orderLine = order.OrderLines |> findOrderLine orderLineId // 2. make a new version of the OrderLine with the new price
    let newOrderLine = {orderLine with Price = newPrice}
    // 3. create a new list of lines, replacing // the old line with the new line
    let newOrderLines =
        order.OrderLines |> replaceOrderLine orderLineId newOrderLine
    // 4. make a new version of the entire order, replacing // all the old lines with the new lines
    let newOrder = {order with OrderLines = newOrderLines}
    // 5. return the new order
    newOrder

The final result, the output of the function, is a new Order containing a new list of lines, where one of the lines has a new price. You can see that immutability causes a ripple effect in a data structure, whereby changing one low-level component can force changes to higher-level components too."

where Order and OrderLine are defined in the book like this:

type Order = {
  Id : OrderId // id for entity 
  CustomerId : CustomerId // customer reference 
  ShippingAddress : ShippingAddress 
  BillingAddress : BillingAddress
  OrderLines : OrderLine list
  AmountToBill : BillingAmount
}
and OrderLine = {
  Id : OrderLineId // id for entity 
  OrderId : OrderId
  ProductCode : ProductCode 
  OrderQuantity : OrderQuantity 
  Price : Price
}

I'm not fully convinced of having the Order aggregate having a changeOrderLinePrice function that takes an orderLineId as one of its parameters... isn't this forcing the clients using this function to know what the id of the order line is in order to change the price?

Also, are we sure that OrderLine should be modelled as an Entity? Can we consider it instead a ValueObject composed by a ProductCode, OrderId, Quantity and Price?

@xpepper
Copy link
Author

xpepper commented Jan 23, 2025

Also, are we sure that OrderLine should be modelled as an Entity? Can we consider it instead a ValueObject composed by a ProductCode, OrderId, Quantity and Price?

DeepSeek R1 response

The decision to model OrderLine as an Entity or Value Object hinges on the domain requirements and the lifecycle of the OrderLine. Let’s break down the trade-offs:


Option 1: OrderLine as a Value Object

If OrderLine is a Value Object, it has no identity and is defined solely by its attributes:
ProductCode, OrderId, Quantity, and Price. Any change to these attributes creates a new OrderLine.

Pros:

  1. Simplicity: No need to manage an OrderLineId or track identity.
  2. Immutability: Aligns with functional programming principles—data is never modified in-place.
  3. Domain Alignment: Works if OrderLine is merely a collection of attributes (e.g., in a read-only order).

Cons:

  1. No Tracking: Changes to individual lines (e.g., updating a price) cannot be tracked over time, as there’s no identity to reference.
  2. Ambiguity Risk: If two lines have identical attributes (e.g., same ProductCode and Price), they are indistinguishable.
  3. Client Coupling: To modify a line, the client must know all its attributes (e.g., ProductCode), which may not be reliable or practical.

Example:

type OrderLine = {
  ProductCode : ProductCode
  OrderId : OrderId
  Quantity : OrderQuantity
  Price : Price
}

// To update a line, replace it entirely in the Order's list:
let changePrice order productCode newPrice =
  { order with
      OrderLines =
        order.OrderLines
        |> List.map (fun line ->
            if line.ProductCode = productCode then
              { line with Price = newPrice } // New Value Object
            else line)
  }

Option 2: OrderLine as an Entity

If OrderLine is an Entity, it has a unique OrderLineId that persists even when its attributes change. This is the approach in the book.

Pros:

  1. Identity: Uniquely reference and track individual lines, even if attributes change.
  2. Explicit Updates: Clients can modify specific lines using OrderLineId, avoiding ambiguity.
  3. Auditability: Track lifecycle events (e.g., price changes) for individual lines.

Cons:

  1. Complexity: Requires managing OrderLineId and ensuring it’s propagated correctly.
  2. Overhead: May be unnecessary if lines never change after creation.

Example:

type OrderLine = {
  Id : OrderLineId // Unique identifier
  ProductCode : ProductCode
  OrderId : OrderId
  Quantity : OrderQuantity
  Price : Price
}

// To update a line, use its Id:
let changePrice order lineId newPrice =
  { order with
      OrderLines =
        order.OrderLines
        |> List.map (fun line ->
            if line.Id = lineId then
              { line with Price = newPrice } // Same Id, updated attributes
            else line)
  }

When to Choose Which Model?

Scenario Value Object Entity
OrderLines never change after creation ✅ Simpler, no need for IDs. ❌ Unnecessary complexity.
OrderLines can be modified ❌ Can’t track changes. ✅ Required for updates.
Need to reference lines uniquely ❌ Ambiguous if attributes match. ✅ Use OrderLineId for precision.
Audit/logging requirements ❌ No way to track individual lines. ✅ Track changes via OrderLineId.
Functional/immutable design ✅ Natural fit for immutability. ✅ Still works (immutable updates via IDs).

Recommendation

The book models OrderLine as an Entity because:

  1. Modifications are allowed: The example includes a changeOrderLinePrice function, implying lines can be updated.
  2. Precision: Using OrderLineId avoids ambiguity when multiple lines have the same ProductCode.
  3. Domain Realism: In most systems, order lines need to be referenced uniquely (e.g., for partial refunds, cancellations, or audits).

However, if your domain treats OrderLine as immutable (e.g., once an order is placed, lines cannot change), a Value Object could work. For example, in a system where orders are read-only snapshots after creation, you might omit OrderLineId.


Final Note

The choice depends on how the domain treats OrderLine:

  • Entity: If the domain cares about the lifecycle of individual lines (e.g., "Update the price of line #3").
  • Value Object: If the domain treats lines as static data (e.g., "These are the items in the order as it was placed").

The book’s design is appropriate for a domain where lines can evolve, but your critique highlights a valid consideration: always validate the model against real-world requirements.

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