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

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?

DeepSeek response

Your concerns about the design choices in the book "Domain Modeling Made Functional" are valid and worth exploring. Let's break down the issues you raised:

1. OrderLine as an Entity vs. Value Object

Entity

An Entity is defined by its identity, which remains consistent over time, even if its attributes change. In the context of an OrderLine, if you consider it an Entity, it implies that the OrderLine has a unique identity (OrderLineId) and can change its attributes (like Quantity or Price) while still being the same OrderLine.

Value Object

A Value Object, on the other hand, is defined by its attributes and does not have a conceptual identity. If two Value Objects have the same attributes, they are considered equal. If you model OrderLine as a Value Object, it would mean that any change to its attributes (like Quantity or Price) would result in a completely new OrderLine.

Which one to choose?

  • Entity: If you need to track changes to an OrderLine over time or if the OrderLine has a lifecycle that is important to your domain, then modeling it as an Entity makes sense. For example, if you need to maintain a history of changes to an OrderLine or if the OrderLine is referenced by other parts of the system, it should be an Entity.

  • Value Object: If the OrderLine is simply a collection of attributes that describe a product in an order and you don't need to track changes to it individually, then modeling it as a Value Object might be more appropriate. In this case, changing any attribute of the OrderLine would result in a new OrderLine instance.

2. changeOrderLinePrice Function and OrderLineId

The function changeOrderLinePrice takes an orderLineId as a parameter, which implies that the client needs to know the OrderLineId to change the price. This design choice has implications:

  • Coupling: This design couples the client to the internal structure of the Order aggregate. The client needs to know the OrderLineId to make changes, which might not be ideal if the client should only interact with the Order aggregate without knowing its internal details.

  • Alternative Design: Instead of exposing the OrderLineId to the client, you could design the Order aggregate to handle changes internally. For example, you could have a method on the Order aggregate that takes a ProductCode and a newPrice, and the Order aggregate would find the corresponding OrderLine and update its price. This way, the client doesn't need to know about the OrderLineId.

3. Immutability and Ripple Effects

The book emphasizes immutability, which is a common practice in functional programming. When you change an OrderLine, you create a new OrderLine instance, and consequently, a new Order instance with the updated OrderLine. This ripple effect is inherent in immutable data structures and ensures that the entire aggregate remains consistent.

4. Revised Design Considerations

Given your concerns, here’s how you might reconsider the design:

Option 1: OrderLine as a Value Object

If you model OrderLine as a Value Object, you would not need an OrderLineId. Instead, the OrderLine would be identified by its attributes (ProductCode, OrderId, Quantity, Price). Changing any attribute would result in a new OrderLine.

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

In this case, the changeOrderLinePrice function would not need an OrderLineId:

let changeOrderLinePrice order productCode newPrice =
    let newOrderLines =
        order.OrderLines
        |> List.map (fun line ->
            if line.ProductCode = productCode then
                { line with Price = newPrice }
            else
                line)
    { order with OrderLines = newOrderLines }

Option 2: OrderLine as an Entity with Encapsulation

If you decide to keep OrderLine as an Entity, you could encapsulate the logic within the Order aggregate to avoid exposing OrderLineId to the client:

let changeOrderLinePrice order productCode newPrice =
    let newOrderLines =
        order.OrderLines
        |> List.map (fun line ->
            if line.ProductCode = productCode then
                { line with Price = newPrice }
            else
                line)
    { order with OrderLines = newOrderLines }

In this design, the client only needs to know the ProductCode and the new price, not the OrderLineId.

Conclusion

The choice between modeling OrderLine as an Entity or a Value Object depends on the specific requirements of your domain. If you need to track changes to individual OrderLines or if they have a lifecycle, then modeling them as Entities makes sense. However, if OrderLines are simply descriptive and do not need to be tracked individually, modeling them as Value Objects might be more appropriate.

Additionally, consider encapsulating the logic within the Order aggregate to avoid exposing internal details like OrderLineId to the client, which can lead to a more maintainable and decoupled design.

@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