5

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.

1
  • 1
    "Are truck stops value objects or entities? They only exist within the context of a truck. However, [...]" - the limited context is not what matters; entities don't have to have global identity, they can have an identity that's only unique within the context they are in (in fact, only Aggregate Roots have global identity). A value object is smth like this: if you can theoretically delete it from a database, and then save in its place a different one that happens to have the same properties, and nothing is affected, then its a value. (Or, at runtime, your code works the same if pass by value) – Filip Milovanović Mar 28 '20 at 3:32
1

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.

1

enter image description here

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.