DDD Domain Modeling of Transportation Module
https://softwareengineering.stackexchange.com/questions/407064
-
09-03-2021 - |
Вопрос
I am in the process of trying to model a transportation module for an ERP type system using C# and EF Core. This service is responsible for managing customer pickups and company-owned truck deliveries. Customer pickups contain a list of orders and trucks contain a list of stops and those stops contain a list of orders.
The primary interface for managing pickups and trucks is through a REST based api. Order creation/update/cancellation events are being received from an ordering module via a service bus queue.
Business Rules
- At order entry, an order is assigned an attribute to specify if it is a customer pickup or delivery via a truck. However, it is up to users within this transportation module to associate those orders to a specific pickup or truck/stop instance.
- An order can be associated to only a single pickup or truck stop at a given time.
- Orders have properties which include a status and shipping metrics (dimensions, weight).
- Orders on a truck cannot exceed a certain volume or total weight.
- Order updates can be received at any time, even after a shipping assignment is made. Those updates can change the shipment type (user decides to pick up order vs. having it shipped to them) or order build which would alter the shipping metrics. If shipping type is changed, order should be unassociated to any current pickup or truck stop.
- Trucks have a status of open/in-transit/closed with each stop on the truck having a status of open/delivered.
- Users can mark a truck as in-transit only if all orders have a status of produced.
- Once truck is in-transit, users mark each stop as delivered once the delivery has been made. Only after all stops are marked as delivered, can the truck status be updated to closed.
- If an order is cancelled, it needs to be automatically removed from either the pickup or truck it may be associated to.
We are using a relational database (SQL Server) for storing the individual entities. My question is really around how to model these various aggregate roots/entities/value objects/domain services/domain events as well as the database tables backing them.
Initial Thoughts
- Have aggregate roots of Pickup, Truck and Order.
- Pickup has a list of value objects containing linked order ids.
- Trucks have a list of stop entities and a list of value objects containing linked order ids, order status and order shipping metrics.
- Other options considered - add nullable foreign keys directly to order that reference a pickup or truck stop; additional option is to have an OrderAssociations table that maps orders to a pickup or truck stop.
In order to enforce business rules for the truck though this is where things get a bit interesting. If adding an order to a truck, the total weight and volume of all other orders needs to be taken into account and an error should be returned if the order will cause the truck to exceed the prescribed thresholds. If an order update is received for an order already associated to a truck and that order will cause the truck to exceed the allowed thresholds an alert needs to be fired and the truck cannot be marked as in-transit until the issue is resolved.
Questions
- When an order update is received, we need to know if it is associated to a pickup or truck stop and if it is, that object will need to be notified. If we have separate tables for PickupOrders and TruckStopOrders, determining the association doesn't exactly seem efficient as a query would need to be made to both tables. In addition, we'd need to load the entire truck of stops/orders to call an update order method on the truck aggregate. How would you recommend this order update be handled? Is this an application level event handler to update the order entity itself as well as the truck? Does the order entity get updated and raise a domain event that the truck is somehow notified of? Curious on thoughts of if this logic belongs in the domain layer or application layer.
- Are truck stops value objects or entities? They only exist within the context of a truck. However, they do have a status associated to them (open/delivered) and a list of associated orders.
- It would be nice for orders to maintain a reference to the pickup or truck stop that they are associated with. However, doing so would couple the truck stop / pickup and order so any order updates would require updating both in the same transaction. Not sure if this would be managed via a domain service or application layer?
- If maintaining a separate truck stop orders table, what is the best way to keep the order status/metrics in this table in sync with the orders table?
Happy to provide additional clarification where needed. Any thoughts are appreciated.
Нет правильного решения
Другие советы
As a general rule of thumb (and especially since you seem to be concerned about the efficiency of loading an entire truck), you want to keep aggregates small and only update a single aggregate in one transaction. This does mean that business rules spanning multiple aggregates will only be eventually consistent, but this doesn't usually cause any practical problems.
Based on this, you might consider turning your current design inside out: instead of pickups and truck stops containing a list of orders, create first-class aggregates (say PickupOrder
and TruckStopOrder
) that model the association of an order to a pickup or a truck stop.
Order context handles UpdateOrderCommand
:
- Update
Order
aggregate. - Publish
OrderUpdated
event.
Truck context handles OrderUpdated
event:
- Update
TruckStopOrder
aggregate. - Publish
TruckStopOrderUpdated
event.
Truck context handles TruckStopOrderUpdated
event:
- Recompute total volume/weight of
Truck
aggregate. - Publish an alert if constraints are violated.
The UI to add an order to a truck can use cached information from the Truck
aggregate to display a warning if the truck is already full. In a concurrent system, this can never be guaranteed to be consistent since other users may be updating the truck at the same time.
Here's how I would domain model this... I usually decompose business requirements into core operations. Once I have identified those core operations, I then identify the sub-domains that would host those operations.
Note, I prefer to identify operations first because it's easier for me to identify what types are required in order for those operations to succeed.
Note:
Just because OOP languages such as C# or Java are general purpose doesn't mean that they are ideal languages for modeling business domains. Thus, I sincerely believe that statically typed FP languages are a natural fit for modeling business domains due to less syntax along with higher information density.
In regards to data persistence, I don't think a relational database is ideal. I would use an event store (i.e. immutable database) so that data cannot be overwritten or deleted. After all, this domain is about operating on historical domain events that should never be updated or deleted (only appended).
I have provided the following model given the description of the domain:
CustomerOrder.Operations
namespace CustomerOrder
open Shared
open Language
module Operations =
type PlaceOrder = PlaceOrderSubmission -> AsyncResult<unit,Error>
type ChangeAcquisition = ChangeAcquisitionSubmission -> AsyncResult<unit,Error>
CustomerOrder.Language
module Language
open Shared
type AuthenticatedCustomer = TODO
type AcquisitionType = string // Ex: CustomerPickup | TruckDelivery
type PlaceOrderSubmission = {
AuthenticatedCustomer : AuthenticatedCustomer
Order : Order
OrderRequestType : AcquisitionType
}
type ChangeAcquisitionSubmission = {
OrderSubmission : PlaceOrderSubmission
NewAcquisitionRequest : AcquisitionType
}
OrderDispatcher.Operations
namespace OrderDispatcher
open Shared
open Language
module Operations =
type AvailableTruckers = AvailableTruckersRequest -> AsyncResult<TruckersOpen,Error>
type DispatchTrucker = DispatchTruckerSubmission -> AsyncResult<unit,Error>
type CancelledOrder = OrderCancellationSubmission -> AsyncResult<OrderCancellationReceipt,Error>
type ChangeOrderAcquisition = OrderAcquisitionChangeSubmission -> AsyncResult<UnstartedOrder,Error>
OrderDispatcher.Language
module Language
open Shared
type TruckerId = string
type Trucker = {
TruckerId : TruckerId
}
type DispatchTruckerSubmission = {
Trucker : Trucker
Order : Order
}
type AvailableTruckersRequest = {
Order : Order
}
Trucker.Operations
namespace Trucker
open Shared
open Language
module Common =
type QueryUnstartedOrders = AuthenticatedTrucker -> AsyncResult<UnstartedOrders,Error>
module NewOrderPending =
type AcceptOrderRequest = OrderResponseSubmission -> AsyncResult<unit,Error>
type DeclineOrderRequest = OrderResponseSubmission -> AsyncResult<unit,Error>
type ForfeitOrderRequest = OrderResponseSubmission -> AsyncResult<unit,Error>
module AcceptedOrder =
type CancelAcceptance = CancellableOrder -> AsyncResult<unit,Error>
type StartInTransit = OrderProduced -> AsyncResult<InTransitToPickupTrucker,Error>
type InTransitToPickup = InTransitToPickupTrucker -> AsyncResult<IntransitToPickupOrder ,Error>
//----------------------------------------------------------------------------------------
// Handle change of how order is acquired (i.e. pickup or delivery)
//----------------------------------------------------------------------------------------
type MyDelegate = delegate of obj * OrderCancelled -> unit
type IOrderCancelled =
[<CLIEvent>]
abstract member OrderCancelled : IEvent<MyDelegate, OrderCancelled>
type IncomingNotification () =
let orderCancelled = new Event<MyDelegate, OrderCancelled> ()
interface IOrderCancelled with
[<CLIEvent>]
member x.OrderCancelled = orderCancelled.Publish
//----------------------------------------------------------------------------------------
module InTransitToDropoff =
type CancelAcceptance = InTransitToDropoffTrucker -> AsyncResult<OrderCancellationReceipt,Error>
type InTransitToDropoff = InTransitToDropoffTrucker -> AsyncResult<IntransitToDropoffOrder ,Error>
type ClaimDelivered = InTransitToDropoffTrucker -> AsyncResult<OrderClosed ,Error>
module OrdersCompleted =
type CloseTruck = CloseTruckSubmission -> AsyncResult<ClosedTruckReceipt,Error>
Trucker.Language
module rec Language
open Shared
type TruckerStatus =
| Open of OpenedTrucker
| InTransit of InTransitTrucker
| Completed of CompletedTrucker
type AcceptedOrder = {
Trucker : OpenedTrucker
}
type IntransitToPickupOrder = {
Trucker : InTransitTrucker
}
type IntransitToDropoffOrder = {
Trucker : InTransitTrucker
}
type CompletedOrder = {
Trucker : CompletedTrucker
}
type OrderResponseSubmission = {
OpenedTrucker : OpenedTrucker
UnstartedOrder : UnstartedOrder
Response : Response
}
type InTransitTrucker = {
Trucker : AuthenticatedTrucker
CurrentOrder : OrderProduced
OrdersProduced : OrderProduced seq
OrdersClosed : OrderProduced seq
}
type InTransitToPickupTrucker = {
Trucker : AuthenticatedTrucker
Order : OrderInTransit
}
type InTransitToDropoffTrucker = {
Trucker : AuthenticatedTrucker
Order : OrderInTransit
}
type CompletedTrucker = {
Trucker : AuthenticatedTrucker
OrdersClosed : OrderProduced seq
}
type ArrivedAtDropoffSubmission = {
Trucker : InTransitTrucker
}
type CancellableOrder =
| OpenedTrucker of OpenedTrucker
| InTransitTrucker of InTransitTrucker
type CloseTruckSubmission = {
OrdersClosed : OrderClosed seq
}
type ClosedTruckReceipt = {
OrdersClosed : OrderClosed seq
}
Shared Language
module Shared
type AsyncResult<'T,'error> = Async<Result<'T,'error>>
type Error = string
type OrderId = string
type TruckerId = string
type CustomerId = string
type ItemId = string
type Name = string
type Description = string
type Response = string
type Address = string
type Weight = float
type feet = float
type AcquisitionType = string
type CancellationId = string
type OrderStatus = string
type Dimensions = {
Length : feet
Width : feet
Height : feet
}
type AuthenticatedTrucker = {
TruckerId : TruckerId
}
type OpenedTrucker = {
Trucker : AuthenticatedTrucker
}
type Item = {
ItemId : ItemId
Name : Name
Description : Description
Weight : Weight
Dimensions : Dimensions
}
type ItemQty = {
Item : Item
Qty : int
}
type ItemQtys = ItemQty seq
type Pickup = {
Address : Address
ItemQtys : ItemQtys
}
type Customer = {
CustomerId : CustomerId
Address : Address
}
type Order = {
OrderId : OrderId
Customer : Customer
Pickup : Pickup
Status : OrderStatus
}
type OrderProduced = {
Order : Order
}
type OrderInTransit = {
OrderProduced : OrderProduced
}
type OrderClosed = {
OrderInTransit : OrderInTransit
}
type OrderCancelled = {
Order : Order
}
type OrderCancellationSubmission = {
Order : Order
Reason : string
}
type OrderCancellationReceipt = {
CancellationId : CancellationId
Order : Order
Reason : string
}
type OrderAcquisitionChangeSubmission = {
Order : OrderCancellationReceipt
AcquisitionType : AcquisitionType
}
type UnstartedOrder = { Order: Order }
type UnstartedOrders = UnstartedOrder seq