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
?
ChatGPT 4o response
This is an excellent question, and it gets to the heart of domain modeling: deciding whether a concept should be modeled as an Entity or a Value Object. Let’s examine whether
OrderLine
should really be treated as an entity in the example.Recap: Entity vs. Value Object
The key question to ask:
Is
OrderLine
an Entity?In the book,
OrderLine
is modeled as an entity, but whether this makes sense depends on your domain:Arguments for
OrderLine
as an Entity:OrderLine
has a lifecycle of its own (e.g., it can be modified, tracked, or deleted independently of theOrder
), it’s a strong candidate for an entity. For example:OrderLine
(e.g., when the quantity was changed, by whom, etc.).OrderLine
has its own business processes (e.g., workflows like inventory reservations or backorders).OrderLineId
(e.g., for auditing or integration with other systems), the line’s identity becomes important. In this case,OrderLine
being an entity makes sense.OrderLines
with the sameProductCode
,Quantity
, andPrice
are still treated as distinct (e.g., they represent separate transactions), thenOrderLine
needs an identity beyond its attributes.Arguments for
OrderLine
as a Value Object:OrderLine
as entirely defined by its attributes (ProductCode
,OrderQuantity
,Price
, etc.), there’s no need for a unique identity. Two OrderLines with the same values are indistinguishable, making them Value Objects.OrderLine
is immutable, any change (e.g., toOrderQuantity
) would result in replacing the oldOrderLine
with a new one. The identity is irrelevant because changes propagate through theOrder
aggregate.OrderLine
exists purely as part of theOrder
aggregate and has no meaningful existence outside of it, modeling it as a Value Object simplifies the design.OrderLine
as a Value Object removes the need to manage an additional ID (OrderLineId
). Instead, its uniqueness within theOrder
can be enforced by a combination ofProductCode
and possiblyOrderId
.When does
OrderLine
work better as a Value Object?Example Scenario
Imagine an e-commerce system where:
Order
is essentially a collection ofOrderLines
, and theOrderLine
is just a way to represent a product (ProductCode
) and its associated quantity and price.OrderLine
independently (e.g., it doesn’t need to log specific changes toOrderLine
attributes or treat them as distinct entities).OrderLine
always flow through theOrder
aggregate.In this case,
OrderLine
can be modeled as:The Order would then manage
OrderLine
as a collection of Value Objects:When updating an
OrderLine
(e.g., changing its price), you simply replace the oldOrderLine
with a new one, maintaining immutability:Trade-offs of Treating OrderLine as a Value Object
OrderLineId
, which reduces complexity and keeps the aggregate focused.OrderLine
naturally propagate through the aggregate, which aligns well with the immutable data structure principles in functional programming.OrderLine
’s identity, treating it as a Value Object better matches the ubiquitous language.When does OrderLine need to be an Entity?
There are cases where
OrderLine
should retain its identity:OrderLine
over time (e.g., who changed the quantity or price, and when), thenOrderLineId
becomes necessary to distinguish between instances.OrderLineId
(e.g., references in external systems), you need to preserve its identity.OrderLine
is involved in complex workflows, like inventory management or shipping logistics, treating it as an entity can make the model more expressive.So, whether
OrderLine
should be an Entity or a Value Object depends entirely on the domain requirements and how the ubiquitous language describes it. If the identity of theOrderLine
matters in your domain, stick with it as an entity. But if theOrderLine
is just a compositional part of theOrder
and defined solely by its attributes, modeling it as a Value Object simplifies the design and aligns better with immutability principles.