Domain Driven Design

Software Process Engineering


Giovanni Ciatto — giovanni.ciatto@unibo.it


Compiled on: 2026-10-15 — printable version

back

Lecture goals

  • Understand how domain-driven design guides the derivation of a software model from a domain
  • Know the DDD building blocks (tactical design) and integration patterns (strategic design)
  • Map a DDD model onto a layered architecture
  • Understand event sourcing and CQRS
  • Practice on the exercises

Outline

  1. Motivation: why a structured design process
  2. Main notions: domain, subdomains, bounded contexts, model, ubiquitous language
  3. Building blocks: entities, value objects, aggregates, repositories, services, domain events, factories, dependency injection
  4. Strategic design: bounded contexts, context maps, integration patterns
  5. Layered architecture: layers, ports & adapters, modules
  6. Event sourcing and CQRS: storing changes, segregating reads and writes
  7. Exercises

Main references: Evans, Domain-Driven Design, 2003, Vernon, Implementing Domain-Driven Design, 2013

Motivation and Context

Why a structured design process?

  • You know programming, in many programming languages

  • You know about object-oriented programming and design patterns

  • You know about software architectures and design principles

  • You know about software engineering best practices


What’s the criterion to choose if and when to adopt languages / patterns / architectures / principles / practices?

The domain as the criterion

Left, facts stated by domain experts; right, the design choice each fact suggests, with the part of the lecture discussing it. A placed order must never change suggests immutable value objects (building blocks). Every price change must be traceable for years suggests event sourcing. Payment and shipping are run by different teams and change at different paces suggests separate bounded contexts (strategic design). The catalogue is browsed all day but updated once a week suggests segregating reads from writes (CQRS). We may switch payment provider next year suggests ports and adapters and dependency injection. Crossed out as non-criteria: it’s trendy, I already know it, the last project used it, the framework wants it so

A linear workflow: the problem is turned into a model via analysis, the model into an architecture via design, and the architecture into a solution via implementation; the model and the steps leading to and from it are highlighted

  • yet, how to derive the model?

Why Domain Driven Design?

  • Here we present domain-driven design (DDD)
    • one of many approaches to software design

  • It consists of principles, best practices, and patterns guiding design
    • unified under a common philosophy
    • focus is on the design workflow, other than the result

  • Major benefits:
    • it stresses adherence to the problem at hand
    • it focuses on delivering a business-tailored model
      • and, therefore, a business-tailored solution
    • it harmonises communication among managers, technicians, and users
    • it stresses the production of maintainable and extensible software

Main notions of DDD

What is the domain?

  • Definition of domain:
    • a well-established sphere of knowledge, influence or activity
    • the subject area to which the user applies the software

  • Remarks:
    • focus is on how users and experts perceive the domain
    • focus is not on how developers perceive the domain

  • Examples of domains and the contexts composing them
    • some university (department, faculty, HR, etc.)
    • some given company (manufacturing, marketing, HR, etc.)
    • linear algebra (matrices, complex numbers, polynomials, etc.)
    • machine learning (classification, regression, feature selection, etc.)

What is the domain? (example)

A university domain made of five contexts, each with its own expert and its own words: teaching (course coordinator), student office (secretary), human resources (HR officer), research (research office), facilities (facilities manager). Some words recur with different meanings: a student takes classes and exams in teaching, but is someone enrolled who pays fees in the student office; a course is a subject taught yearly in teaching, but a degree programme in the student office; a professor teaches courses in teaching, is an employee with a contract in HR, and a principal investigator in research

DDD Philosophy (pt. 1)

  • Software will represent a solution to a problem in some business domain
    • it should be modelled & implemented to match that domain
      • i.e. modelling should elicit the key aspects of a domain, possibly by interacting with experts
      • i.e. design and implementation should mirror the domain

  • Words do not have meaning per se, but rather w.r.t. a domain
    • i.e. the same word may have different meanings in different domains
    • each domain comes with a particular language characterising it
      • software components (interfaces, classes, etc.) should be named after that language
    • interaction with experts is essential to identify a domain’s language

DDD Philosophy (pt. 2)

  • Software should adhere to the domain, at any moment
    • architecture and implementation should favour adherence to the domain
      • in spite of their evolution / modification

  • Functionalities, structure, and UX should mirror the domain too

    • using the libraries should be natural for developers
    • UX should be natural for users

    as both developers and users are (supposed to be) immersed in the domain

Overview of main notions

  • Domain: the reference area of knowledge

  • Subdomain: a portion of the domain (problem space)

  • Bounded context: the boundary within which a model and its language are consistent (solution space)

  • Model: a reification of the domain in software

  • Ubiquitous Language: language used by domain experts and mirrored by the model

Overview of main notions (picture)

Problem space: an e-commerce domain split into payment (generic), offer (core), customer (supporting), and shipping (supporting) subdomains. Solution space: each subdomain is realised, ideally one-to-one, by a bounded context, which contains its own model and its own ubiquitous language

Subdomains and Bounded Contexts

A bounded context is a boundary within which a model and its language are consistent:

  • relying on a sub-set of the concepts of the domain
  • where words/names have a unique, precise meaning
  • clearly distinguishable from other contexts
  • ideally 1 subdomain $\approx$ 1 bounded context, but not necessarily

Kinds of subdomains

  • core: where the business differentiates itself $\rightarrow$ invest most modelling effort here
  • supporting: needed, specific to the business, not differentiating
  • generic: common to many businesses (e.g. authentication, invoicing) $\rightarrow$ buy / reuse
  • e.g. for a university: teaching may be core, course scheduling supporting, authentication generic

(cf. Evans, 2003; Evans, DDD Reference, 2015)

Domain vs. Subdomains vs. Contexts

An e-commerce domain split into payment (generic), offer (core), customer (supporting), and shipping (supporting) subdomains, each realised by one bounded context with its services, except shipping, realised by a shipping and a tracking context. Every context has its own Customer model, all linked to the same customer identity (CustomerID): as payer in payment (billing address, tax code or VAT number, payment methods), as shopper in the catalog (purchase history, preferences, segment), as account holder in the customer context (personal data, credentials, contacts), and as recipient in shipping (shipping addresses, delivery preferences) and tracking (shipments to track, notification contacts)

Same concept, different models

  • The same real-world concept (e.g. a customer) shows up in several subdomains, seen from a different angle each time:

    • payment $\rightarrow$ the customer as payer: billing (fiscal) address, tax code / VAT number, payment methods
    • offer $\rightarrow$ the customer as shopper: purchase history, preferences, segment $\Rightarrow$ predict offers and recommendations
    • customer $\rightarrow$ the customer as account holder: personal data, credentials, contacts, which they manage themselves
    • shipping $\rightarrow$ the customer as recipient: shipping addresses, delivery preferences, shipments to track
  • Each bounded context keeps its own model of the concept, with only the data and behaviour it needs

    • possibly under a different name, in its own ubiquitous language (payer, shopper, recipient, …)
  • What contexts share is the identity (e.g. CustomerID), not the model

    • contexts refer to each other’s objects by identifier
    • the same holds for other concepts, e.g. a product: price and discounts in offer, VAT rate in payment, weight and size in shipping
  • Anti-pattern: one all-encompassing Customer class shared by every context

    • any change for one context affects all the others, and the constraints of different contexts end up conflicting
    • how contexts integrate with each other is the subject of context maps

The domain Model

Set of software abstractions mapping relevant concepts of the domain

  • e.g. Java/Kotlin/Scala projects, packages, interfaces, classes, records, methods, etc.

The Ubiquitous Language

  • A language structured around the domain model
    • used by all people involved in the domain
    • which should be used in the software
    • in such a way that their semantics is preserved
  • underlying assumption:

    • different people call the same things differently
    • especially when they come from different contexts
  • commonly reified into a glossary of terms

  • used to name software components

How a glossary may look like

Excerpt of a glossary for an e-commerce domain, as a table with columns term, context, definition, and synonyms to avoid. Basket (offer): the products a customer is considering, not binding, not to be confused with order. Order (offer): a binding request to buy products at the agreed prices. Place an order (offer): turn a basket into an order. Customer appears twice, with different meanings: in payment, whoever is billed (a.k.a. payer); in shipping, whoever receives a shipment (a.k.a. recipient). Shipment (shipping): a parcel carrying part of one order to one address

  • one entry per term and per context: the same word may mean different things in different contexts

The glossary is reflected everywhere

The glossary terms (Basket, Order, place, Customer, Shipment) are mirrored by the code (interfaces Basket, Order, Customer, method place, List of Shipment), by the documentation (a page explaining how a customer places a basket, which becomes an order delivered via shipments), and by conversations among a domain expert, a developer, and a manager, who all use the same terms

  • same words, same meanings, for all people: experts, developers, managers, users

Conceptual Workflow

  1. Identify the domain, give a name to it

  2. Identify the main contexts within the domain, giving them names

    • possibly, by interacting with experts
  3. Identify the actual meaning of relevant words from the domain, and track them into a glossary

    • possibly, by interacting with experts

    • without assuming you already know the meaning of words

      • i.e. do not rely on (your) common sense
    • keep in mind that the meaning of words may vary among contexts

      • homonyms: similar names, different meanings
      • synonyms: different names, similar meanings
  4. Adhere to the language, use it, make it yours

    • especially when talking about the domain / model / software
    • design/sketch code mirroring the language
  5. Draw a context map (defined later) tracking

    • the main contexts and their junctions
    • words whose meaning varies across contexts
  6. Model the software around the ubiquitous language

    • rule of thumb: 1 concept $\approx$ 1 interface

Conceptual Workflow (activity diagram)

skinparam defaultFontSize 18
skinparam ActivityBackgroundColor #e3edf8
skinparam ActivityBorderColor #2f6db5
start
:Identify and name the **domain**;
:Identify and name the main **contexts**;
repeat
  :Elicit the meaning of relevant **words**
  with **domain experts**
  (beware of homonyms and synonyms);
  :Update the **glossary**;
repeat while (ambiguous or missing words?) is (yes)
-> no;
:Adopt the **ubiquitous language**
in talks, sketches, and code;
:Draw the **context map**;
:Model the software around the language
(1 concept ≈ 1 interface);
(A)
note right: continues with\nbuilding blocks

Discovering the domain: Event Storming

  • Collaborative workshop with domain experts and developers (cf. Brandolini)
  • Put domain events (past tense) on a timeline, then add commands, actors, aggregates, and policies
  • Boundaries between clusters of events suggest bounded contexts
  • Domain events are introduced later

More on event storming in the course Software Architecture and Platforms

Event Storming: notation

Sticky-note colours used in event storming: orange for domain events (e.g. Order Placed), blue for commands (Place Order), small yellow for actors (Customer), large yellow for aggregates (Order), lilac for policies (whenever … then …), green for read models (Basket Summary), pink for external systems (Payment Gateway), and red, tilted, for hot spots (open questions)

  • one colour per kind of sticky note, on a long paper roll (or a virtual whiteboard)

Event Storming: big picture

Domain events of an e-commerce placed along a timeline: product added to or removed from basket, order placed, payment authorized or refused, invoice issued, shipment prepared, parcel dispatched, delivered, or returned. Hot spots ask what if stock runs out after placing, and who pays for returns. Dashed boundaries group the events into candidate offer, payment, and shipping contexts

  1. everybody writes the events they know, then the group sorts them in time
  2. discussions reveal hot spots and the words people use $\rightarrow$ glossary
  3. clusters of events suggest bounded contexts

Event Storming: process modelling

Process-level event storming: a Customer, informed by the Basket Summary read model, issues the Place Order command, handled by the Order aggregate, which emits Order Placed; a policy (whenever an order is placed, charge the customer) issues the Charge Customer command, sent to the Payment Gateway external system, which emits Payment Authorized, and so on

  • zoom into one process: actor $\rightarrow$ command $\rightarrow$ aggregate $\rightarrow$ event $\rightarrow$ policy $\rightarrow$ command …
  • aggregates and events found here become building blocks of the model

DDD Building Blocks

Towards building blocks

Modelling maps the domain onto the model: a domain concept (e.g. customer) becomes a type (e.g. interface Customer), and each instance of the concept (e.g. Alice, Bob) is reified as an object of that type (alice, bob)

  • Each concept from each context shall become a type in the model
    • type $\approx$ class, interface, structure, ADT, etc.
      • depends on what the programming language has to offer

  • Use building blocks as archetypes
    • let them guide and constrain your design

Workflow

(continued)

  1. Choose the most adequate building block for each concept

    • depending on the nature of the concept
    • … or the properties of its instances
  2. The building block dictates how to design the type corresponding to the concept

    • objects in OOP are shaped by types
  3. The choice of building block may lead to the identification of other concepts / models

    • e.g. entities may need value objects as identifiers
    • e.g. entities may need repositories to be stored
    • e.g. entities may need factories to be created
    • e.g. aggregates may be composed of entities or value objects
skinparam defaultFontSize 18
skinparam ActivityBackgroundColor #e3edf8
skinparam ActivityBorderColor #2f6db5
(A)
repeat
  :Pick a **concept**;
  :Choose its **building block**;
  :Design the corresponding **type**;
  if (new concepts emerged?) then (yes)
    :Add them to the **glossary**
    and to the model;
  else (no)
  endif
repeat while (concepts left?) is (yes)
-> no;
stop

Building blocks (overview)

  • Entity: objects with an identifier

  • Value Object: objects without identity

  • Aggregate: cluster of entities/value objects with a consistency boundary, accessed via its root entity

  • Repository: objects providing storage facilities

  • Service objects: providing stateless functionalities

  • Domain Event: objects modelling relevant events (notifications)

  • Factory: objects creating other objects

    • they enable dependency injection (and, hence, testability)

Building blocks (concept)

A concept map linking model-driven design to services, entities, value objects, aggregates, factories, and repositories; entities act as aggregate roots and repositories access aggregates

Building blocks at two levels

Building blocks are archetypes for design decisions: they apply at (at least) two levels of abstraction

  • Low level, i.e. OOP modelling: types, classes, interfaces, and objects within a software process
    • the focus of the original DDD literature, and of the first slides of each building block
  • High level, i.e. Web APIs and microservices: resources, data schemas, endpoints, messages, and services of a distributed system
    • each building block ends with suggestions on how to employ it at this level

Building block OOP level Web API / microservices level
Value Object immutable data class / record reusable schema, embedded by value, no URL of its own
Entity class with an identifier schema with an id + resource with its own URL
Aggregate cluster of objects behind a root resource (+ sub-resources), transaction boundary of each request
Repository object hiding the storage DBMS technology, and which service owns which data
Service stateless object wiring the domain microservice ($\approx$ one per bounded context), API controllers
Domain Event time-stamped value object message schema, propagated via a broker
Factory object creating other objects POST on collections, request $\rightarrow$ domain object mapping

Entities vs. Value Objects

Genus-differentia definition:

  • genus: both can be used to model elementary concepts
  • differentia: entities have an explicit identity, value objects are interchangeable

Quick modelling examples

Classroom

  • Seats in a classroom may be modelled as value objects

  • Attendees of a class may be modelled as entities

Seats on a plane

  • Numbered seats $\rightarrow$ entities

  • otherwise $\rightarrow$ value objects

Entities vs. Value Objects (constraints)

Value Objects

  • Identified by their attributes
    • equality compares attributes alone
  • Must be immutable: their state is their attributes, which never change
    • read-only properties
    • lack of state-changing methods
  • May be implemented as
    • structures in .NET
    • data classes in Kotlin, Scala, Python
    • records in Java
  • Must implement equals() and hashCode() on JVM
    • implementation must compare the objects’ attributes

Entities vs. Value Objects (constraints)

Entities

  • They have an inherent identity, which never changes during their lifespan
    • common modelling: identifier attribute, of some value type
    • equality compares identity
  • Can be stateful $\Rightarrow$ may have a mutable design
    • modifiable properties
    • state-changing methods
  • May be implemented via classes in most languages
  • Must implement equals() and hashCode() on JVM
    • implementation must compare (at least) the objects’ identifiers

Entities vs. Value Objects (example)

interface Customer {
    + CustomerID getID()
    ..
    + String getName()
    + void **setName**(name: String)
    + String getEmail()
    + void **setEmail**(email: String)
}
note left: Entity

interface CustomerID {
    + Object getValue()
}
note right: Value Object

interface TaxCode {
    + String getValue()
}
note left: Value Object

interface VatNumber {
    + long getValue()
}
note right: Value Object

VatNumber -d-|> CustomerID
TaxCode -d-|> CustomerID

Customer *-r- CustomerID

Value Objects at the Web level

  • Value objects $\rightarrow$ reusable data schemas
  • They have no URL of their own: they are always embedded by value into the representations of other resources
  • Their invariants become schema constraints: required, pattern, minimum, enum, additionalProperties: false, …
    • servers reject non-conforming requests with 400 Bad Request (or 422 Unprocessable Content)
  • The schema is the contract between clients and servers, the record is its counterpart in the code
  • Immutability comes for free: representations are copies, so updating a value means replacing it
# openapi.yaml (excerpt)
components:
  schemas:
    Money:
      type: object
      required: [amount, currency]
      additionalProperties: false
      properties:
        amount:   { type: number, minimum: 0 }
        currency: { type: string, pattern: "^[A-Z]{3}$" }
    Address:
      type: object
      required: [street, city, zip]
      additionalProperties: false
      properties:
        street: { type: string, minLength: 1 }
        city:   { type: string, minLength: 1 }
        zip:    { type: string, pattern: "^[0-9]{5}$" }
record Money(BigDecimal amount, Currency currency) {
    Money { // compact constructor: checks invariants
        if (amount.signum() < 0)
            throw new IllegalArgumentException("negative amount");
    }
}

record Address(String street, String city, String zip) {
    Address {
        if (street.isBlank() || city.isBlank())
            throw new IllegalArgumentException("blank field");
        if (!zip.matches("[0-9]{5}"))
            throw new IllegalArgumentException("invalid zip");
    }
}

Entities at the Web level

  • Same as value objects (i.e. data schemas), plus identity
    • the identifier is part of the schema, but readOnly: assigned by the server, never changed by clients
    • the identifier is also part of the entity’s URL, e.g. /customers/{id}
    • prefer opaque, server-generated identifiers (e.g. UUIDs) over natural keys (e.g. tax codes)
      • natural keys may change, and they leak personal data into URLs and logs
  • Entities are mutable $\rightarrow$ concurrent updates may conflict
    • use ETag / If-Match headers for optimistic concurrency (cf. RFC 9110)
    • 412 Precondition Failed if the entity changed since the client last read it
  • Not every entity deserves its own URL: only aggregate roots do (cf. next section)
    CustomerID:
      type: string
      format: uuid
    Customer:
      type: object
      required: [id, name, email]
      properties:
        id:
          $ref: "#/components/schemas/CustomerID"
          readOnly: true
        name:    { type: string, minLength: 1 }
        email:   { type: string, format: email }
        address: { $ref: "#/components/schemas/Address" }
record CustomerID(UUID value) { }

class Customer {
    private final CustomerID id; // never changes
    private String name, email;  // may change
    private Address address;     // value object

    // ... constructor, getters, setters ...

    @Override public boolean equals(Object o) {
        return o instanceof Customer c && id.equals(c.id);
    }
    @Override public int hashCode() { return id.hashCode(); }
}

Aggregates and Aggregate Roots

Definition

  • An aggregate is a cluster of entities/value objects treated as a unit for consistency and changes

  • The aggregate root is the single entity in the aggregate that outside objects may reference

  • It guarantees the consistency of the objects it contains

  • It mediates the usage of the composing objects from the outside

  • Outside objects should avoid holding references to composing objects

(cf. Evans, 2003; Vernon, 2013)

Aggregate Roots (constraints)

  • They are usually compound entities

  • They can be or exploit collections to contain composing items

  • May be better implemented as classes in most programming languages

  • Must implement equals() and hashCode() on JVM (as any other entity)

    • equality compares the root’s identity, as for any entity

Aggregate Roots (constraints, cont’d)

  • Components of an aggregate should not hold references to components of other aggregates
    • that’s why they are called aggregate roots
    • notable exception: references to identifiers of other aggregates

Three aggregates, each with an aggregate root and internal components; references from one aggregate to another aggregate’s root are allowed, while a reference to another aggregate’s internal component is forbidden

Aggregate Root (example)

package "Customer aggregate" <<Rectangle>> {
    interface Customer {
        + CustomerID getID()
        ..
        + String getName()
        + String getEmail()
    }
    note left of Customer: aggregate root

    interface CustomerID
    note left of CustomerID: value object
}

package "Order aggregate" <<Rectangle>> {
    interface Order {
        + OrderID getID()
        + CustomerID **getCustomer**()
        + Instant getTimestamp()
        + Address getShippingAddress()
        + Iterable<OrderItem> getItems()
        ..
        + void **setShippingAddress**(address: Address)
        + void **addItem**(product: ProductID, amount: long)
        + void **removeItem**(product: ProductID)
        + Money getTotal()
    }
    note top of Order: aggregate root

    interface OrderID
    note bottom of OrderID: value object

    interface Address {
        + String getStreet()
        + String getCity()
    }
    note bottom of Address: value object

    interface OrderItem {
        + ProductID getProduct()
        + long getAmount()
        + Money getPrice()
    }
    note bottom of OrderItem: entity
}

Customer "1" *-d- "1" CustomerID
Order "1" *-d- "1" OrderID
Order "1" *-d- "1" Address
Order "1" *-d- "N" OrderItem
Order .l.> CustomerID: refers to\nthe customer\nby ID

(notice that Order refers to its Customer via the CustomerID only, and that OrderItems refer to products via ProductIDs)

Aggregates at the Web level (pt. 1)

Recall: ReST principles and API design anti-patterns in these slides

  • One aggregate root $\rightarrow$ one collection resource + one item resource
    • e.g. /orders (collection) and /orders/{orderId} (item)
  • Internal components $\rightarrow$ sub-resources, reachable only through the root
    • e.g. /orders/{orderId}/items/{productId}, never /order-items/{id}
  • References to other aggregates $\rightarrow$ identifiers (or links), never embedded copies
    • e.g. "customerId": "c-42", or "customer": "/customers/c-42", in the representation of an order
  • GET on the root returns the whole aggregate (root + components)
    • consistently with repositories, which load and store whole aggregates
  • One request modifies one aggregate $\rightarrow$ the aggregate is the transaction boundary of each request
    • changes spanning several aggregates are eventually consistent, via domain events
  • Domain operations which are not CRUD $\rightarrow$ model them as resources (nouns), not as verbs in the URL
    • e.g. POST /orders/{orderId}/cancellation, rather than POST /cancelOrder?id=...
  • The root still enforces the invariants, and violations become status codes
    • e.g. adding items to a cancelled order $\rightarrow$ 409 Conflict

Aggregates at the Web level (pt. 2)

Example: Web API for the Order aggregate

Method Path Meaning Order method Success Errors
GET /orders?customerId=c-42 list orders (of a customer) (repository query) 200 400
POST /orders place a new order (factory) 201 + Location 400, 422
GET /orders/{orderId} read the whole aggregate getters 200 + ETag 404
PUT /orders/{orderId}/shipping-address replace a value object setShippingAddress 204 400, 404, 412
POST /orders/{orderId}/items add an item, via the root addItem 201 400, 404, 409
DELETE /orders/{orderId}/items/{productId} remove an item, via the root removeItem 204 404, 409
POST /orders/{orderId}/cancellation domain operation cancel (new) 201 404, 409
DELETE /orders/{orderId} delete the aggregate (repository) 204 404

  • Paths mention aggregates and their components, never database tables or actions
  • Every endpoint on /orders/{orderId}/... is handled by loading the root, invoking one of its methods, and saving it back

Repositories (definition)

Objects mediating the persistent storage/retrieval of other objects

A client gets an aggregate from a repository, modifies it, and saves it back; the repository rebuilds (hydrates) the aggregate from data loaded from the database, and stores it there, dealing with whole aggregates only

Repositories (details)

Purpose

  • Hiding (i.e. be backed by) some database technology
  • Possibly realising some sort of object-relational mapping (ORM)
  • Storing / retrieving aggregate roots as wholes
  • Supporting CRUD operations on aggregate roots, and/or wrapping common queries
    • Create, Read, Update, Delete

Remarks

  • They may exploit factories for turning retrieved data into objects
  • If properly engineered, they avoid the lock-in effect for database technologies
  • Design & implementation may require thinking about:
    • the architecture,
    • the infrastructure,
    • the expected load,
    • etc.

Repositories (constraints)

  • They are usually identity-less, stateful, and composed objects

    • state may consist of the stored objects
    • state may consist of DB connections
  • May be implemented as classes in most OOP languages

  • Provide methods to

    • add, remove, update aggregate root entities
    • select and return one or more entities, possibly in a lazy way
      • this should return Iterable, Collection, or Stream on JVM
  • Non-trivial implementations should take care of

    • enforcing consistency, in spite of concurrent access
    • supporting complex transactions

Repositories (example)

interface CustomerID

interface Customer

Customer "1" *-u- "1" CustomerID

interface CustomerRegistry {
    + Iterable<Customer> getAllCustomers()
    ..
    + Customer findCustomerById(CustomerID id)
    + Iterable<Customer> findCustomerByName(String name)
    + Iterable<Customer> findCustomerByEmail(String email)
    ..
    + void addNewCustomer(Customer customer)
    + void updateCustomer(Customer customer)
    + void removeCustomer(Customer customer)
}

CustomerRegistry "1" o--> "N" Customer: contains
CustomerRegistry --> CustomerID: exploits

Repositories at the Web level (pt. 1)

  • At the Web level, repositories are mostly about infrastructure
    • which DBMS technology?
      • relational (e.g. PostgreSQL), document (e.g. MongoDB), key-value (e.g. Redis), graph (e.g. Neo4j), …
      • depending on the shape of aggregates, and on the queries to be supported
      • aggregates fit document stores naturally: one document per aggregate
    • how to deploy it? managed (cloud) vs. self-hosted, replication, backups, …
  • Each microservice may adopt a different technology: polyglot persistence (cf. Fowler)
  • The repository interface keeps the choice swappable (and mockable in tests, cf. dependency injection)
skinparam defaultFontSize 18
rectangle "Shared database" {
  component "Order\nservice" as O1
  component "Customer\nservice" as C1
  database "DB" as DB1
  O1 --> DB1
  C1 --> DB1
}
rectangle "Database per service" {
  component "Order\nservice" as O2
  component "Customer\nservice" as C2
  database "Orders\nDB" as DB2
  database "Customers\nDB" as DB3
  O2 --> DB2
  C2 --> DB3
  O2 ..> C2 : API / events
}

The main design decision, though, is: shall microservices share a database?

Repositories at the Web level (pt. 2)

Shared database vs. database per service

Aspect Shared database Database per service
Coupling via the schema: one change may break other services each service owns its data, hidden behind its API
Deployment and scaling coordinated independent
Queries across contexts easy (e.g. SQL joins) API composition, or local read models (cf. CQRS)
Transactions across contexts ACID, for free sagas + eventual consistency
Technology one DBMS for all polyglot persistence
Operations one DB to run many DBs to run, back up, monitor

Repositories at the Web level (pt. 3)

  • Rule of thumb: one database per bounded context (cf. microservices.io)

    • sharing a DBMS server is fine, sharing tables is not
      • e.g. one schema (and one DB user) per service, on the same PostgreSQL instance
    • a shared database is a hidden shared kernel (cf. strategic design): acceptable only with tight coordination among teams
  • Consequences of having one database per service:

    • data of other contexts is either requested via their APIs, or replicated locally by listening to their domain events
    • business transactions spanning several services become sagas (cf. microservices.io)
      • i.e. sequences of local transactions, with compensating actions in case of failure
      • e.g. place order $\rightarrow$ reserve stock $\rightarrow$ charge payment; if payment fails, release stock and cancel order
    • saving an aggregate and publishing its event must be atomic $\rightarrow$ transactional outbox (cf. microservices.io)
      • the event is stored in the same DB transaction as the aggregate, then relayed to the broker

Services

Operations that do not naturally belong to any entity or value object

Purpose

  • Reifying control-related aspects of the software

  • Wiring aggregates, entities, and value objects together

  • Exposing coarse-grained functionalities to the users

  • Providing a façade for the domain

  • Making the business logic evolvable, interchangeable, replaceable

  • domain service: domain logic spanning several aggregates; lives in the domain layer

  • application service: orchestrates a use case (loads aggregates via repositories, calls them, saves, publishes events); lives in the application layer

  • Rule of thumb: put behaviour on entities/aggregates first; a model of only getters/setters plus services is an anemic domain model (cf. Fowler)

Remarks

  • Services may be exposed via RESTful APIs
  • Should be designed taking current use cases into account (i.e. design services to be purpose-specific)
    • entities/objects should support future use cases, too (i.e. design entities/objects to be general purpose)

Services (constraints)

  • They are usually identity-less, stateless objects

  • May be implemented as classes in OOP languages

    • or bare functions in functional languages
  • Commonly provide procedures to do business-related stuff

    • e.g. a complex operation…
      • … concerning some particular aggregate root
      • … which does not support the operation directly through its methods
        • (maybe because the operation is use-case specific)
    • e.g. proxying an external service
    • e.g. a complex operation spanning several aggregates, repositories, factories, etc.
  • Non-trivial implementations should take care of

    • supporting concurrent access to the service’s facilities
    • exposing domain events to the external world

Services and events

  • Services are most commonly prosumers of events

    • i.e. they both produce and consume events
    • events are most commonly domain events (cf. later)
  • In practice, services commonly implement the observer pattern (aka event listener)

    • often in both roles: event source (subject) and event listener (observer)
  • Hence, services may come with ad-hoc methods to

    • produce events, i.e. let listeners (un)subscribe
      • e.g. addOrderEventListener(listener), removeOrderEventListener(listener)
    • react to events, i.e. handle notifications from other sources
      • e.g. onCustomerEvent(event)

Services (example)

interface OrderManagementService {
    + void performOrder(Order order)
    ..
    + void **addOrderEventListener**(OrderEventListener listener)
    + void **removeOrderEventListener**(OrderEventListener listener)
}

interface OrderEventListener {
    + void **onOrderEvent**(OrderEvent event)
}

interface CustomerEventListener {
    + void **onCustomerEvent**(CustomerEvent event)
}

interface Order {
    + OrderID getId()
    + CustomerID getCustomer()
    + void setCustomer(CustomerID customer)
    + Instant getTimestamp()
    + void setTimestamp(Instant timestamp)
    + Map<ProductID, Long> getAmounts()
}

interface OrderID

interface CustomerID

interface ProductID

interface OrderStore

Order "1" *-r- "1" OrderID
Order "1" *-d- "1" CustomerID
Order "1" *-u- "N" ProductID
OrderStore "1" *-- "N" Order

OrderManagementService ..> Order: handles
OrderManagementService ..> OrderStore: updates
OrderManagementService -u-|> CustomerEventListener
OrderManagementService "1" o-l- "N" OrderEventListener: notifies

note bottom of OrderStore: repository
note top of Order: entity (aggregate root)
note right of OrderID: value object
note right of ProductID: value object
note right of CustomerID: value object
note right of OrderManagementService: service\n(source of order events,\nlistener of customer events)
note left of OrderEventListener: listener (observer)
note left of CustomerEventListener: listener (observer)

OrderID -u[hidden]- ProductID
OrderID -d[hidden]- CustomerID

Services at the Web level (pt. 1)

  • Which and how many Web services? Rule of thumb: one microservice per bounded context

    • the bounded context is the boundary of a model, of a language, and (often) of a team
    • finer splits (e.g. one service per aggregate) are possible, but increase the communication overhead
    • coarser splits are fine to begin with: a modular monolith may be split later (cf. Fowler)
  • Inside each microservice:

    • application services $\rightarrow$ API controllers (i.e. endpoint handlers)
      • parse requests, load aggregates via repositories, invoke them, save them, publish events
    • domain services stay internal: not every service object deserves an endpoint
  • Microservices communicate either:

    • synchronously, via ReST calls
      • simple, but the caller depends on the callee being up (temporal coupling)
    • asynchronously, via some message-oriented middleware (i.e. a broker)
      • e.g. RabbitMQ, Apache Kafka, MQTT brokers
      • producers publish domain events on topics/queues, consumers subscribe to them
      • it is the observer pattern, at the system level: publishers know nothing about subscribers

Services at the Web level (pt. 2)

A client calls an API gateway via HTTP; the gateway routes ReST requests to the Customer, Order, and Recommendation services, one per bounded context, each with its own database; the Customer service publishes customer events and the Order service publishes order events on a message broker; the Order service consumes customer events, the Recommendation service consumes order events

Domain Events (definition)

A value-like object capturing some domain-related event
(i.e., an observable variation in the domain, which is relevant to the software)

  • actually, only the event notification/description is reified to a type

A customer places an order via the Order Management Service, which saves the Order aggregate and then publishes an OrderEvent of kind “placed”; the event notifies a Recommendation Service, which updates the Recommendations aggregate

Domain Events (details)

Purpose

  • Propagate changes among portions of the domain model (e.g. contexts, aggregates, entities, etc.)
  • Record changes concerning the domain

Remarks

  • Strong relation with the observer pattern (i.e. publish-subscribe)

  • Strong relation with the event sourcing approach (described later)

  • Strong relation with the CQRS pattern (described later)

Domain Events (constraints)

  • They are usually time-stamped value objects

  • May be implemented as data-classes or records

  • They represent a relevant variation in the domain

    • e.g. a change in the state of some entity / repository
  • Event sources & listeners shall be identified too

    • who is generating the event?
    • who is consuming the event?
  • Infrastructural components may be devoted to propagate events across contexts

    • e.g. a message broker, a message queue, etc.
  • Common convention: one type per event, named in the past tense (e.g. OrderPlaced, OrderCancelled) (cf. Vernon, 2013)

  • [Teacher’s Suggestion]: use a neutral type (e.g. OrderEvent) plus a kind/discriminator field when many events share the same payload

Domain Events (example)

interface OrderManagementService {
    + void performOrder(Order order)
    ..
    + void **addOrderEventListener**(OrderEventListener listener)
    + void **removeOrderEventListener**(OrderEventListener listener)
}

interface OrderEventListener {
    + void **onOrderEvent**(OrderEvent event)
}

interface RecommendationService

interface OrderEvent {
    + String **getKind**()
    + OrderID getOrder()
    + CustomerID getCustomer()
    + Instant **getTimestamp**()
    + Map<ProductID, Long> getAmounts()
}

interface OrderID

interface CustomerID

interface ProductID

OrderEvent "1" *-r- "1" OrderID
OrderEvent "1" *-d- "1" CustomerID
OrderEvent "1" *-d- "N" ProductID
OrderID -[hidden]d- ProductID

OrderManagementService .r.> OrderEvent: publishes
OrderEventListener <|-d- RecommendationService
OrderManagementService "1" o-d- "N" OrderEventListener: notifies
OrderEventListener .r.> OrderEvent: consumes

note top of OrderEvent
    domain event
    (kind: "placed",
    "cancelled", ...)
end note
note right of OrderID: value object
note right of ProductID: value object
note left of CustomerID: value object
note top of OrderManagementService: service
note right of RecommendationService: service
note left of OrderEventListener: listener (observer)

Domain Events at the Web level (pt. 1)

  • As for value objects, mostly a matter of data schema design

    • but for messages exchanged via a broker, rather than for resources exposed via HTTP
  • AsyncAPI is the counterpart of OpenAPI for message-driven APIs

    • it describes the channels (i.e. topics/queues), the operations of each service on them (send/receive), and the messages
    • message payloads are described via JSON Schema, as in OpenAPI
    • documentation and code skeletons can be generated from it, as for OpenAPI
  • CloudEvents standardises the envelope of events, regardless of the broker

    • i.e. the metadata wrapping the payload: id, source, type, time, data, …
  • Events carry identifiers of aggregates (as ReST representations do), and possibly a snapshot of relevant data (cf. Fowler)

    • thin events (identifiers only): consumers query the producer’s API for details
    • fat events (state-carrying): consumers keep a local copy of the data they need, with no further calls

Domain Events at the Web level (pt. 2)

# asyncapi.yaml (excerpt)
asyncapi: 3.0.0
info: { title: Order service events, version: 1.0.0 }
channels:
  orderEvents:
    address: orders.events
    messages:
      orderEvent: { $ref: "#/components/messages/OrderEvent" }
operations:
  publishOrderEvent:
    action: send
    channel: { $ref: "#/channels/orderEvents" }
components:
  messages:
    OrderEvent:
      payload:
        type: object
        required: [kind, orderId, customerId, timestamp, amounts]
        properties:
          kind:       { type: string, enum: [placed, cancelled] }
          orderId:    { type: string, format: uuid }
          customerId: { type: string, format: uuid }
          timestamp:  { type: string, format: date-time }
          amounts:    # ProductID -> amount
            type: object
            additionalProperties: { type: integer, minimum: 1 }
record OrderEvent(
    String kind, // "placed", "cancelled", ...
    OrderID orderId,
    CustomerID customerId,
    Instant timestamp,
    Map<ProductID, Long> amounts
) { }

  • Events are published to unknown consumers $\rightarrow$ their schema is a public contract
    • prefer additive changes (new optional fields)
    • never remove/rename fields, nor change their meaning
    • version the schema otherwise
    • consumers should ignore unknown fields (cf. tolerant reader)
  • Brokers commonly deliver messages at least once $\rightarrow$ consumers should be idempotent
    • e.g. discard events whose id was already processed

Factories (definition)

Objects aimed at creating other objects


A client delegates entity creation to a factory, which hides the complexity of creating the entity

Factories (details)

Purpose

  • Factories encapsulate the creation logic for complex objects

    • making it evolvable, interchangeable, replaceable
  • They ease the enforcement of invariants

  • They support dynamic selection of the most adequate implementation

    • while hiding the actual implementation choice

Remarks

  • DDD’s notion of factory is quite loose
    • DDD’s Factories $\supset$ GOF’s Factories $\cup$ Builders $\cup$ …

Factories (constraints)

  • They are usually identity-less and state-less objects

  • May be implemented as classes in most OOP languages

  • Provide methods to instantiate entities or value objects

  • Usually they require no mutable field/property

  • No need to implement equals() and hashCode() on JVM

Factories (example)

interface CustomerID

interface TaxCode

interface VatNumber

interface Customer

Customer "1" *-- "1" CustomerID

VatNumber -u-|> CustomerID
TaxCode -u-|> CustomerID

enum Gender {
    MALE
    FEMALE
}

interface CustomerFactory {
    + TaxCode computeTaxCode(String name, String surname, LocalDate birthDate, Gender gender, String birthPlace)
    ..
    + Customer newCustomerPerson(TaxCode code, String fullName, String email)
    + Customer newCustomerPerson(String name, String surname, LocalDate birthDate, Gender gender, String birthPlace, String email)
    ..
    + Customer newCustomerCompany(VatNumber code, String fullName, String email)
}
note bottom of CustomerFactory
- method for computing tax codes
  (birth place = Italian municipality, e.g. "Roma" -> H501)
- methods for creating person customers
- methods for creating company customers
end note

CustomerFactory .l.> Gender

CustomerFactory -r-> TaxCode: creates
CustomerFactory -u-> Customer: creates

Factories at the Web level (pt. 1)

  • POST on a collection resource is the Web counterpart of a factory

    • the client sends the ingredients, the server creates the entity
    • the server assigns the identifier, computes derived values (e.g. tax codes, totals, timestamps), and enforces invariants
    • response: 201 Created, plus Location: /customers/{id} header, plus the representation of the new entity
    • conversely, PUT /customers/{id} lets the client choose the identifier (sensible only for natural keys)
  • Different factory methods $\rightarrow$ different request schemas for the same collection

    • e.g. newCustomerPerson vs. newCustomerCompany $\rightarrow$ oneOf + discriminator in OpenAPI
  • Request bodies are untrusted representations (aka DTOs, data transfer objects), not domain objects

    • factories reconstitute valid domain objects out of them, or reject them (400 / 422)
    • this keeps the wire format and the domain model independent of each other
  • POST is not idempotent: if the client retries after a timeout, duplicates may be created

    • idempotency keys: the client sends a unique Idempotency-Key header, the server creates the entity only once (cf. IETF draft)

Factories at the Web level (pt. 2)

# openapi.yaml (excerpt)
paths:
  /customers:
    post:
      requestBody:
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/NewPerson"
                - $ref: "#/components/schemas/NewCompany"
              discriminator: { propertyName: kind }
      responses:
        "201":
          description: customer created
          headers:
            Location:
              schema: { type: string, format: uri }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Customer" }
        "422":
          description: invalid data (e.g. malformed VAT number)
// API controller (application layer)
Customer create(NewCustomerRequest request) {
    Customer customer = switch (request) {
        case NewPerson p -> factory.newCustomerPerson(
            p.name(), p.surname(), p.birthDate(),
            p.gender(), p.birthPlace(), p.email());
        case NewCompany c -> factory.newCustomerCompany(
            factory.vatNumber(c.vat()), // may throw
            c.fullName(), c.email());
    };
    registry.addNewCustomer(customer);
    return customer;
    // -> 201 Created
    //    Location: /customers/{customer.getID()}
}
  • NewPerson and NewCompany are DTOs (e.g. records generated from the OpenAPI schemas)
  • factory and registry are the CustomerFactory and CustomerRegistry from the previous examples

Factories enable Dependency Injection

  • Services depend on repositories and factories (and possibly on other services)
    • e.g. OrderManagementService needs an OrderStore to save orders
  • Who creates these dependencies?
    • if the service calls new SqlOrderStore() itself, it is bound to that implementation
    • … and it can hardly be tested without a real database

Left: without DI, the service itself creates a SqlOrderStore, so the implementation choice is hard-coded in the client. Right: with DI, a factory first creates the SqlOrderStore and then injects it into the service, which only uses it through the OrderStore interface

  • Factories already hide the implementation choice
    • Dependency Injection (DI) goes one step further: the client does not create its dependencies, it receives them

Dependency inversion

Goal: minimise dependencies among types, to make implementations interchangeable

  • Let interfaces depend on interfaces only
  • Let classes depend on interfaces, never on other classes
    • built-in types (e.g. String, LocalDate) are tolerated exceptions
  • Delay the choice of the actual implementation of each interface as much as possible
  • Do not leak implementation types into public APIs

Before: OrderManagementServiceImpl depends directly on the SqlOrderStore class. After: OrderManagementServiceImpl depends on the OrderStore interface, which is implemented by both SqlOrderStore and InMemoryOrderStore, so the arrows towards the implementation are inverted

Dependency Injection (concept)

Interfaces are separated from classes; the wiring between them is delayed and performed outside the classes using them

  1. Design & segregate interfaces
    • interface members only expose interface (or built-in) types: natural after DDD
    • interfaces and implementations in separate modules (e.g. :domain vs. :storage)
  2. Implement classes relying on interfaces only
    • fields, parameters, return types: interfaces (private members too)
    • expose a mechanism to inject the actual implementations:
      • constructor injection: dependencies are constructor parameters
        • the object is always complete and consistent $\rightarrow$ preferred
      • setter injection: one setter per dependency
        • more flexible, yet objects may be left in inconsistent states
  3. Wire interfaces and classes at runtime
    • choosing the most adequate implementation for the situation at hand (production, test, etc.)

Inversion of Control

  • Without DI, an object controls the creation of its own dependencies
  • With DI, this control is inverted: some external entity creates the dependencies and passes them in
    • the composition root: the (only) place where implementations are chosen and wired
      • e.g. the main method, or an application-level factory
    • or a DI container (a.k.a. IoC container), i.e. a library doing the wiring for us
  • Hollywood principle: “don’t call us, we’ll call you”
  • Dependency Injection is a particular form of Inversion of Control (IoC)

Remark

  • A factory alone just moves the new somewhere else: the client now depends on the factory
  • It becomes DI when the factory is the composition root, i.e. it creates the objects and injects them into each other

DI in Java (example, pt. 1): implementation

public interface OrderStore { void add(Order order); /* ... */ }

class OrderManagementServiceImpl implements OrderManagementService {
    private final OrderStore store;          // interface
    private final CustomerFactory customers; // interface

    // constructor injection: dependencies are received, not created
    OrderManagementServiceImpl(OrderStore store, CustomerFactory customers) {
        this.store = Objects.requireNonNull(store);
        this.customers = Objects.requireNonNull(customers);
    }

    @Override
    public void performOrder(Order order) {
        // ... validate the order, possibly via customers ...
        store.add(order);
        // ... then notify order event listeners ...
    }

    // ... (un)subscription methods for order event listeners ...
}
  • The service only mentions interfaces: OrderStore, CustomerFactory, Order
  • No new for any dependency $\rightarrow$ the implementation choice is somebody else’s business

DI in Java (example, pt. 2): the composition root

public class ApplicationFactory { // the composition root
    public OrderStore orderStore() {
        return new SqlOrderStore(System.getenv("DB_URL"));
    }
    public CustomerFactory customerFactory() {
        return new CustomerFactoryImpl();
    }
    public OrderManagementService orderManagementService() {
        return new OrderManagementServiceImpl(orderStore(), customerFactory());
    }
}

// in main():
OrderManagementService service = new ApplicationFactory().orderManagementService();
  • The factory is the only class mentioning implementation classes
    • which can stay package-private (not leaked into the API)
  • A hand-written factory is a minimal DI container
    • one method per interface, specifying which implementation and how to initialise it
  • In a layered architecture, this is the job of the outermost module (cf. modules)

DI eases testability

  • The class under test does not create its dependencies $\rightarrow$ the test decides what to pass in
    • no need to edit production code, nor to patch it with static mocking tricks

Unit tests: inject test doubles (stubs, fakes, mocks)

@Test
void performOrderStoresTheOrder() {
    OrderStore store = mock(OrderStore.class); // Mockito
    var service = new OrderManagementServiceImpl(
        store, new CustomerFactoryImpl());
    service.performOrder(order);
    verify(store).add(order);
}
  • tests the service in isolation, no DB needed

Integration tests: inject real (yet lightweight) components

@Test
void orderIsActuallyStored() {
    var store = new InMemoryOrderStore();
    var service = new OrderManagementServiceImpl(
        store, new CustomerFactoryImpl());
    service.performOrder(order);
    assertTrue(store.contains(order));
}
  • Each test acts as a test-specific composition root

DI eases testability (overview)

The same OrderManagementServiceImpl code receives an OrderStore via its constructor in three different set-ups: in production it is wired by ApplicationFactory with a SqlOrderStore; in unit tests it is wired by the test with a Mockito mock; in integration tests it is wired by the test with an InMemoryOrderStore or a SqlOrderStore on a test database run with Testcontainers

  • Only the wiring changes across production, unit tests, and integration tests
    • the production code is tested as is
  • Dependencies that are hard to control in tests (DB, network, clock, randomness) should be injected as well
    • e.g. inject a java.time.Clock instead of calling LocalDateTime.now()

DI technologies

  • Hand-written factories become tedious as the dependency graph grows
  • DI containers automate the wiring. They let developers:
    1. register the chosen implementation for each interface (a.k.a. binding)
    2. specify its lifetime: singleton (one shared instance), transient (new instance per request), scoped (one per scope, e.g. per HTTP request)
    3. retrieve (or have injected) an object for a given interface
Technology Language Notes Docs
Jakarta Dependency Injection (ex JSR-330) Java standard annotations (@Inject, @Singleton), no container spec
Jakarta CDI Java standard container of Jakarta EE (e.g. Quarkus) spec
Spring Framework Java, Kotlin the most widespread container, runtime reflection docs
Google Guice Java bindings declared in modules, runtime docs
Dagger 2 / Hilt Java, Kotlin (Android) wiring code generated at compile time docs
Koin Kotlin (multiplatform) Kotlin DSL, no reflection docs
Microsoft.Extensions.DependencyInjection C# IServiceCollection + IServiceProvider docs
  • Containers support overriding bindings in tests too
    • e.g. Spring’s @MockitoBean, Guice’s Modules.override(...)

Strategic Design

Towards DDD patterns

Further notions involving contexts

  • Bounded Context: enforce a model’s boundaries & make them explicit

  • Context Map: providing a global view on the domain and its contexts

Actual definitions

Context Boundary

The boundary of a context and its software model should be explicit. This is helpful from several perspectives:

  • technical (e.g., dependencies among classes/interfaces)
  • physical (e.g., common database, common facilities)
  • organizational (e.g. people/teams maintaining/using the code)

Context Map

A map of all the contexts in a domain and their boundaries

  • and their points of contact
    • e.g. their dependencies, homonyms, false friends, etc.
  • providing the whole picture of the domain

Example of bounded context map

A sales context containing Opportunity, Pipeline, Territory, and Sales Person, connected by Customer and Product to a support context containing Ticket, Defect, and Product Version

(source: Fowler)

Bounded Contexts & Context Maps (best practices)

  • Clearly identify & represent boundaries among contexts

  • Avoid responsibility diffusion over a single context

    • one responsible person / team for each context
  • Avoid changes in the model for problems arising outside the context

    • rather, extend the domain by creating new contexts
  • Enforce context’s cohesion via automated unit and integration testing

    • to be (re)executed as frequently as possible

Model integrity problem

How to preserve the integrity of the model?

  • As the domain evolves, the software model should evolve with it

    • in order to maintain the coupling
  • Yet, the domain rarely changes as a whole

    • more commonly, it changes in a context-specific way
  • Contexts are bounded, but not isolated

    • so are models, which may depend on each other
  • Changes to a context and its model may propagate to other contexts / models

Domain / model changes are critical and should be done carefully

Model integrity patterns

  • Shared kernel: sharing a common model among contexts
  • Customer–supplier: the downstream (customer) team requests changes in the supplier model
  • Conformist: one model’s team reacts to changes of some model they depend on
  • Anti-corruption layer: a model’s team isolates itself from another model

Purposes

  • Preserve the integrity of the model w.r.t. the domain

  • Minimise the potential impact / reach of changes

    • each context should be as independent as possible
    • each change affects as few contexts as possible

Model integrity patterns (background, pt. 1)

Three contexts, A, B, and Legacy, each enclosing one or more models; lines between models of different contexts mark contact points between contexts

  • Context maps highlight relations among contexts
    • yet, not all relations are equal, nor symmetric

Model integrity patterns (background, pt. 2)

A downstream Search Page Context and an upstream Search API Context connected by a line, marked D and U at the two ends

Each relation among 2 contexts usually involves 2 ends/roles:

  • upstream end, i.e. the one providing functionalities
  • downstream end, i.e. the one consuming functionalities
    • the downstream depends upon the upstream, but not vice versa

Integration among contexts $\leftrightarrow$ interaction among teams

  • several strategies may be employed, depending on
    • mutual trust* among teams
    • ease of communication/cooperation among teams
    • technical / organizational / administrative / legal constraints



*trust $\approx$ willingness to collaborate + seek stability

Shared Kernel

Catalog and Inventory contexts overlap around a shared supplier model used by both contexts

  • Best when: multiple contexts share the same team / organization / product

  • Key idea: factorise common portions of the model into a shared kernel

  • Upstream and downstream collaborate in designing / developing / maintaining the model

    • they are peers
  • Keeping the kernel as small as possible is fundamental

Customer–Supplier

A downstream Search Page Context and upstream Search API Context connected by a customer-supplier relationship

  • Best when:

    • multiple teams
    • mutual trust
    • good communication
  • Key idea:

    • upstream acts as supplier, downstream acts as customer
    • both sides collaborate to maximise integration among their models
      • and interoperability among their software
  • Customers may ask for features, suppliers will do their best to provide them

  • Suppliers shall warn before changing their model

Conformist

A Search Page Context downstream of a Search API Context, while an External Context conforms downstream to the Search API Context

  • Best when:

    • multiple teams
    • poor communication / different pace
    • some trust
  • Key idea: downstream must conform to the upstream, reactively

    • adapting their model accordingly
    • whenever the upstream’s one changes

Anti-corruption layer

A Search Context isolates its model from an upstream Catalog Context through an anti-corruption translation layer with no business logic

  • Best when:

    • multiple teams
    • poor communication
    • poor trust
  • If upstream cannot be trusted, and interaction is pointless…

    • e.g. legacy code, poorly maintained library, etc.
  • … downstream must defend itself against unexpected / unanticipated change

  • The upstream’s model is then reverse engineered & adapted

    • e.g. often, repository types are anti-corruption layers for DB technologies

Layered (onion) Architecture

Layered Architecture (disclaimer)

  • DDD does not enforce a particular architecture

  • Any is fine as long as the model keeps its integrity

  • Layered architectures are well suited to preserve models’ integrity

  • The same dependency rule (outer depends on inner, never the converse) underlies hexagonal (ports & adapters, cf. Cockburn, 2005), onion, and clean architectures

    • in hexagonal terms, domain/application define ports (e.g. repository interfaces); outer layers provide adapters (e.g. DB-backed repositories, REST controllers)

Layered (onion) architecture (concept)

Concentric layers: Domain at the centre, Application and Presentation around it, and Messages, REST API, and Storage as outer adapters

  • outer layers depend on inner ones
    • the converse is not true

Layered (onion) architecture (explanation)

  1. Domain layer: contains the domain model (entities, values, events, aggregates, etc.)

    • must support a wide range of applications
    • has no dependency on any other layer
  2. Application layer: contains services providing business logic

    • supports a particular use case via services
    • depends on the domain layer
  3. Presentation layer: provides conversion facilities to/from representation formats

    • e.g. JSON, BSON, XML, YAML, XDR, Avro, HTML, etc.
      • depends on the domain layer (and, possibly, on the application layer)
  4. Storage layer: supports persistent storage/retrieval of domain data

    • this is where repositories are implemented
    • may involve some DB technology
    • depends on the domain layer (and, possibly, on the application layer)
  5. Interface layers (e.g. ReST API, MOM, View): let external entities access the software

    • via a GUI, or via some remote interface such as HTTP

Enforcing the architecture in the code

  • Layering may be enforced in the code

  • By mapping layers into modules

    • module $\approx$ packaging unit
      • e.g. Gradle sub-projects, Maven modules, .NET assemblies, etc.
    • each module having its own build dependencies
top to bottom direction

component ":domain" as domain
component ":application" as application
component ":presentation" as presentation
component "third-party serialization library" as gson
component "third-party DB client library" as db
component ":storage" as storage
component ":web-api" as server
component ":message-queue" as mq
component ":command-line" as cli
component product

application ..> domain
presentation ..> application
server ..> presentation
mq ..> presentation
cli ..> presentation
storage ..> application
presentation ..> gson
storage ..> db
product ..> server
product ..> storage
product ..> mq
  • product is the composition root: it wires implementations from all modules (cf. Dependency Injection)

Advanced aspects of DDD

Event sourcing (preliminaries)

  • Whenever there is a mutable entity …
  • … whose state evolution over time must be tracked …
  • … state transitions can be memorised in 2 ways:
    • one may track the current state
    • or the flow of variations

Two timelines for a counter: state snapshots record values 0, 1, 2, while a flow records initial value 0 followed by increment events that reconstruct those values

Event sourcing

A pattern where domain events are reified into time-stamped data and the whole evolution of a system is persistently stored

  • perfect match with DDD as domain events are first-class citizens

(cf. Fowler, Event Sourcing)

Event sourcing (pros & cons)

Benefits

  • Historical data can be analysed, to serve several purposes

    • e.g. predictive maintenance, optimisation, analysing & anticipating faults
  • Past situations can be replayed

    • it improves debugging and enables measurements
  • Enables complex event detection & reaction

  • Enables CQRS (described later)

Limitations

  • A lot of data is generated and must be stored, which costs space
  • Reconstructing the (current) state costs time

Command–Query Responsibility Segregation (CQRS)

  • Advanced pattern for building highly-scalable applications

  • It is often combined with event sourcing, but does not require it (cf. Fowler, CQRS)

  • It can deliver reactive, eventually consistent solutions where:

    • context boundaries can be easily enforced
    • single responsibility principle is applied extensively

CQRS definition

Split the domain and application layers to segregate read/write responsibilities

(cf. Fowler, CQRS; Fowler, Event Sourcing)

  • Read model (a.k.a. view or query model)

    • accepts queries aimed at observing the state of the system
  • Write model (a.k.a. command model)

    • accepts commands aimed at altering the state of the system

CQRS concept

A UI sends commands to the command model, which validates them, applies domain logic, and writes to the database; the query model reads from the database and serves query results to the UI

(source: Fowler)

CQRS workflow (writing)

Whenever users want to perform an action in the system:

  1. they create a command and forward it to the write model
  • i.e. an object describing a variation to be applied to some domain aspect

  1. the command is validated by the write model; if accepted, it produces one or more domain events
  • with event sourcing, the events (not commands) are appended to an event store

CQRS workflow (reading)

Whenever users want to inspect/observe the system at time $t$:

  1. they perform a query on the read model
    • asking for the state of the system at time $t$
    • e.g. $t$ $\equiv$ now

  1. the read model is a projection built from the events up to time $t$
    • it returns a read-optimised representation of the system state at time $t$

CQRS – When is the read model updated?

A projection computes a read model by applying the events recorded up to time $t$

  • If read and write data are stored on different databases

    • projecting events updates the query database
    • the query database should be read-efficient
    • the event store should be write-efficient
  • Several, non-mutually-exclusive strategies:

    • eager: events are projected as soon as they are received
    • on read: events are projected when a query is read
    • scheduled: events are projected in the background, periodically

Exercises

Repository: https://github.com/unibo-spe/ddd-exercise, branch exercise

(solutions on branch master)

Each exercise folder contains a walkthrough.md, suggesting how to address the exercise step by step

Exercise 1 – Simple Store (pt. 1)

A simple domain keeping track of: customers, products, and orders.

Customers

  • Customers can either be companies or persons

  • Companies are identified by a VAT number

  • Persons are identified by tax codes

    • computed out of name, surname, birth date, gender, and birth place
  • In any case a name and email are recorded for each customer

  • Both the name and email may vary over time

Products

  • Products are identified by name and type/category

  • They have a nominal price, expressed using some currency

  • The availability of each product is tracked as well

  • Both the price and the available quantity may vary over time

Exercise 1 – Simple Store (pt. 2)

Money and exchange rates

  • Money is represented by a decimal value and a currency

  • Currencies are represented by a name, a symbol and an acronym

    • e.g. Euro, EUR, €
    • e.g. US Dollar, USD, $
  • Exchange rates keep track of the conversion rate

    • from a source currency
    • to a destination currency
    • at a particular moment
  • Information about exchange rates can be obtained from the Internet

  • We can compute the price of each product, in any currency, at any moment

Exercise 1 – Simple Store (pt. 3)

Orders

  • Orders are identified by a serial number

  • They keep track of the products ordered by a customer

    • and the number of copies ordered for each product
  • Also, orders keep track of when they have been performed

  • All such information may be modified before the order is delivered

  • When a new order is registered, many actions should be performed in reaction

  • It must be possible to compute the actual total price of an order

    • at a particular moment, using a particular currency

Exercise 1 – Simple Store (pt. 4)

TO-DO

  1. Read informal domain description
  2. Identify the main domain concepts composing the ubiquitous language
  3. Model the domain as Java types (classes or interfaces)
    • the model should include entities, value objects, repositories, factories, and services
  4. Structure the Java types according to some module structure compliant with hexagonal architecture
    • i.e. put code into either the domain or application modules
  5. Sketch tests and, then, implementation for at least one
    • entity
    • value object
    • factory
    • repository
    • service
  6. Wire services, repositories, and factories via constructor injection
    • write at least one integration test injecting an in-memory repository

Exercise 2 – Trivial CQRS (pt. 1)

  • Very simple repository type: the Counter
  • It contains 1 long number, initially set to 0
  • The value may be read or changed arbitrarily
  • Whenever the value changes, a new domain event is published, of type Variation
  • However, the repository only memorises the current value of the counter

Exercise 2 – Trivial CQRS (pt. 2)

TO-DO

  1. Switch the design towards event sourcing, by memorising variations instead of snapshots.
  2. Implement the CQRS pattern by splitting the repository into 2 parts:
  • a write-model for storing variations
  • a read-model for retrieving a snapshot of the counter’s value at a given moment

In practice:

  1. provide implementations for the CounterReader and CounterWriter interfaces
  2. optionally, test those implementations

Exercise 3 – Anti-corruption layer (pt. 1)

Exercise 3 – Anti-corruption layer (pt. 2)

TO-DO

  1. Extend the model of our domain with new interfaces supporting CSV parsing / writing functionalities
  2. Design the interfaces so that they are agnostic of the third-party libraries
    • without corrupting the domain model with library-specific types
  3. Provide implementations for the interfaces, using one of the two libraries
  4. Sketch tests for the implementations
  5. Provide implementations for the interfaces, using the other library
  6. Use the same tests as above to prove the two implementations work the same way

Exercise 4 – Dependency Injection (pt. 1)

  • A ParkingLot lets cars enter and exit, by plate

    • the first 15 minutes are free, then each started hour costs 1.50 EUR
    • the same car cannot enter twice
    • drivers are greeted on a display (i.e. the console)
  • The class works, but it is hard to test

    • it reaches by itself everything it needs
    • its tests are slow (they wait for time to pass), weak (no exact fees), and share state

Exercise 4 – Dependency Injection (pt. 2)

TO-DO

  1. Run the tests, and observe what makes them slow or weak
  2. Identify the hidden dependencies of ParkingLot
    • static methods with side effects, static state, I/O, …
    • hint: the system clock is a dependency too (cf. java.time.Clock)
  3. Introduce one interface per dependency (unless the JDK already provides one), and move the current behaviour into implementations
  4. Let ParkingLot receive its dependencies via constructor injection
  5. Let Main be the composition root
  6. Rewrite the tests with test doubles (stubs, fakes, mocks), asserting exact fees, without waiting