Chapter 7 - Using the Language: An Extended Example
Introducing the Cargo Shipping System
Initial requirements:
Track key handling of customer cargo: The system must record actions such as unloading or delivery
Booking cargo in advance: Customers should be able to reserve shipping slots
Automated invoicing: Invoices should be generated when specific handling events occur (ex. when cargo reaches a certain milestone)
In Figure 7.1:
No arrows or associations yet: The diagram in this chapter is still a work in progress, with associations and relationships to be added later
The model serves as a way to organise domain knowledge and create a common language for the development team
Roles and events:
Customers’ multiple roles: A customer might play different roles in relation to the same cargo, such as shipper, receiver, or payer
Handling events: These represent actions that happen to the cargo (ex. unloading, delivering)
Delivery goal: Cargo has a "delivery specification" that outlines its delivery requirements
Refining the model:
The Cargo object could potentially manage its delivery specification, but this may lead to a bloated object with too many responsibilities
Carrier movement: Refers to the trip the cargo takes between locations
The model refinement is expected to be an ongoing process, improving as the development progresses
Isolating the Domain: Introducing the Applications
The application introduces layered architecture to ensure that responsibilities are organised and not scattered across the system
The application layer focuses on coordinating different operations and preventing business logic from being mixed in
3 client functions for the application layer:
Tracking: This function provides access to the past or current handling history of a particular cargo
Booking: It allows users to register new cargo shipments into the system
Incident Logging: This logs handling events that occur for a cargo during its journey
Role of application classes:
These classes act as coordinators. They are responsible for asking the right questions (ex. for tracking or booking purposes)
The application classes should not contain logic to answer these questions themselves. That responsibility lies within the domain layer, where the business logic resides
Distinguishing Entities and Value Objects
Entities represent objects with a distinct identity that persists over time and can change state
Customer:
Can be either a person or a company
Each customer is assigned a unique ID when they first interact with sales, distinguishing them from others
Cargo:
Each cargo is assigned a unique tracking ID to differentiate individual shipments, making it an identifiable entity
Handling event and carrier movement:
These reflect real-world events (e.g., loading or unloading cargo)
They are used to track individual events, ensuring no two events happen simultaneously to the same cargo
Location:
Different places, even with the same name, are not interchangeable in this domain. Location as an entity allows the system to manage location-specific concerns
Delivery history:
Represents the unique journey of a specific cargo
No independent identity but derives identity from the cargo it belongs to. Each cargo has its own delivery history
Value objects do not have an identity but represent attributes or concepts that can be shared across entities
Delivery specification:
Two different cargoes can share the same delivery specification (ex. destination, delivery conditions)
However, each cargo maintains its own delivery history that is used to meet the specification, emphasising that history is not shared between cargoes
Roles and other attributes (ex. role or timestamps):
These are descriptive and do not require a history or continuity
They are used to describe the entity or value object without the need for distinct identity or tracking over time
Designing Associations in the Shipping Domain
Initial diagram shows bidirectional associations (no arrows)
Use traversal direction to reflect domain insights
Cargo holds references to customers (not the other way around), allowing cargo searches by customer via database queries
Cargo manages only its identity, avoiding clutter from responsibilities like delivery specification
There's a circular reference: Cargo → Delivery History → Handling Events → Cargo
Solution 1: Make Delivery History a list of events in code
Solution 2: Use database lookups for infrequent history access; for frequent access, use a direct pointer
Aggregate Boundaries
Customer, Location, and Carrier Movement are entities shared by many cargoes, making them aggregate roots
Cargo is a clear aggregate root, and its boundary includes:
Delivery History: Always tied to the cargo, so it fits within the cargo's aggregate
Delivery Specification: A value object, no issue including it within cargo's boundary
Handling Event: Can be queried independently (ex. to prepare for a Carrier Movement), and has meaning outside a specific cargo
Therefore, Handling Event should be its own aggregate root
Selecting Repositories
Only aggregate roots have repositories
Customer: Needs a Customer Repository to assign roles (shipper, receiver, etc.)
Location: Needs a Location Repository for selecting destinations
Carrier Movement: Needs a Carrier Movement Repository to track loading of cargo
Cargo: Needs a Cargo Repository to manage cargo loading
Handling Event: Implemented as a collection for now, with no need yet to query by Carrier Movement
Walking Through Scenarios
Change destination of a cargo:
Delivery Specification (a value object) is easily replaced by creating a new one
Repeat business:
Customers often repeat similar shipments
Use the Prototype Pattern:
Find a previous cargo in the repository and create a new one based on it
Be cautious not to violate constraints while copying (since Cargo is an aggregate root).
Key considerations when copying a cargo:
Delivery History: Create a new one; the old history doesn’t apply
Customer Roles: Likely the same, so copy references
Tracking ID: Must generate a new one
Delivery Specification: Should be copied to avoid respecification by the user
Rule: Do not modify anything outside the aggregate boundary
Object Creation
Cargo:
Created with an empty delivery history and a null delivery specification, even from a prototype
Two-way association between Cargo and Delivery History: They need to be constructed together
Cargo (aggregate root) can create the Delivery History, which can take Cargo as a parameter in its constructor
Handling Event:
As an entity, all attributes that define its identity (Cargo, completion time, event type) must be passed to the constructor
Non-identifying attributes can be added later, though there are none in this case
Factory methods for each event type may enhance expressiveness and free the client from implementation details
Cycle:
Cargo → Delivery History → Handling Event → Cargo
Delivery History holds a collection of Handling Events, and a back-pointer to Cargo must be created to avoid inconsistency
Alternative Design of the Cargo Aggregate
Frequent refactoring improves model and design based on new insights
Updating Delivery History when adding a Handling Event involves the Cargo aggregate:
Concurrency issues: Modifying Cargo while someone else is also accessing it can delay or fail transactions
Solution: Replace the event collection with a query to avoid locking Cargo
Likely already using a query in the implementation
Optimizes frequent queries (ex. for the most recent events)
Impact:
Delivery History becomes stateless (no longer a collection, just a query)
If access is frequent, using a collection (as in an object database) may be faster than querying
Modules
Infrastructure:
Grouped by type: Entity, Value, Service
Results in low cohesion (objects not conceptually related) and high coupling (associations run between modules)
Domain:
Should be grouped based on cohesive concepts like Customer, Shipping, and Billing
Module names should reflect the domain and become part of the Ubiquitous Language
Sales and marketing handle customers, operations manage shipping, and the back office handles billing
Introducing a New Feature: Yield Management
Feature Goal: Allocate cargo bookings based on goals and profitability, avoiding over/under-booking
Process:
Booking application checks current bookings from the Cargo Repository and available allocations from the Sales Management System
Integration:
Sales Management System uses a different model
Use an anticorruption layer to translate between systems without polluting the ubiquitous language
Allocation Checker: Service class to translate cargo types and check allocations
If further integration is needed (ex. customer database), create separate translators
Enterprise Segment:
Add Cargo type to the domain, translated from sales categories via the Allocation Checker
Enterprise Segment class defines a way to break down the business and queries the Cargo Repository
Performance Tuning:
Minimize message exchanges with caching for Enterprise Segment (static data), while always checking current allocation
Tradeoff between complexity and performance optimization
A Final Look
Why not make Cargo derive the Enterprise Segment?
Reason: Enterprise Segment is specific to allocation; other parts of the business may use different segments
Sales strategy could redefine segments, requiring flexibility
Cargo should not depend on the Allocation Checker—outside its responsibility
Alternative:
Cargo could use a Strategy pattern from the Allocation Checker
This approach adds complexity but can be implemented later if needed