Graduate Program KB

Domain Driven Design

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