giovanni.ciatto@unibo.itCompiled on: 2026-10-15 — printable version
Main references: Evans, Domain-Driven Design, 2003, Vernon, Implementing Domain-Driven Design, 2013
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?
Functionalities, structure, and UX should mirror the domain too
as both developers and users are (supposed to be) immersed in the domain
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
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
The same real-world concept (e.g. a customer) shows up in several subdomains, seen from a different angle each time:
Each bounded context keeps its own model of the concept, with only the data and behaviour it needs
What contexts share is the identity (e.g. CustomerID), not the model
Anti-pattern: one all-encompassing Customer class shared by every context
Set of software abstractions mapping relevant concepts of the domain
- 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:
commonly reified into a glossary of terms
used to name software components
Identify the domain, give a name to it
Identify the main contexts within the domain, giving them names
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
keep in mind that the meaning of words may vary among contexts
Adhere to the language, use it, make it yours
Draw a context map (defined later) tracking
Model the software around the ubiquitous language
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
More on event storming in the course Software Architecture and Platforms
(continued)
Choose the most adequate building block for each concept
The building block dictates how to design the type corresponding to the concept
The choice of building block may lead to the identification of other concepts / models
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
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
Building blocks are archetypes for design decisions: they apply at (at least) two levels of abstraction
| 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 |
Genus-differentia definition:
- genus: both can be used to model elementary concepts
- differentia: entities have an explicit identity, value objects are interchangeable
Seats in a classroom may be modelled as value objects
Attendees of a class may be modelled as entities
Numbered seats $\rightarrow$ entities
otherwise $\rightarrow$ value objects
equals() and hashCode() on JVM
equals() and hashCode() on JVM
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
components/schemas part of an OpenAPI specification, or via JSON Schemarequired, pattern, minimum, enum, additionalProperties: false, …
400 Bad Request (or 422 Unprocessable Content)# 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");
}
}
readOnly: assigned by the server, never changed by clients/customers/{id}ETag / If-Match headers for optimistic concurrency (cf. RFC 9110)412 Precondition Failed if the entity changed since the client last read it 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(); }
}
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)
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)
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)
Recall: ReST principles and API design anti-patterns in these slides
/orders (collection) and /orders/{orderId} (item)/orders/{orderId}/items/{productId}, never /order-items/{id}"customerId": "c-42", or "customer": "/customers/c-42", in the representation of an orderGET on the root returns the whole aggregate (root + components)
POST /orders/{orderId}/cancellation, rather than POST /cancelOrder?id=...409 ConflictOrder 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 |
/orders/{orderId}/... is handled by loading the root, invoking one of its methods, and saving it backObjects mediating the persistent storage/retrieval of other objects
They are usually identity-less, stateful, and composed objects
May be implemented as classes in most OOP languages
Provide methods to
Iterable, Collection, or Stream on JVMNon-trivial implementations should take care of
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
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?
| 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 |
Rule of thumb: one database per bounded context (cf. microservices.io)
Consequences of having one database per service:
Operations that do not naturally belong to any entity or value object
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)
They are usually identity-less, stateless objects
May be implemented as classes in OOP languages
Commonly provide procedures to do business-related stuff
Non-trivial implementations should take care of
Services are most commonly prosumers of events
In practice, services commonly implement the observer pattern (aka event listener)
Hence, services may come with ad-hoc methods to
addOrderEventListener(listener), removeOrderEventListener(listener)onCustomerEvent(event)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
Which and how many Web services? Rule of thumb: one microservice per bounded context
Inside each microservice:
Microservices communicate either:
A value-like object capturing some domain-related event
(i.e., an observable variation in the domain, which is relevant to the software)
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)
They are usually time-stamped value objects
May be implemented as data-classes or records
They represent a relevant variation in the domain
Event sources & listeners shall be identified too
Infrastructural components may be devoted to propagate events across contexts
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
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)
As for value objects, mostly a matter of data schema design
AsyncAPI is the counterpart of OpenAPI for message-driven APIs
CloudEvents standardises the envelope of events, regardless of the broker
id, source, type, time, data, …Events carry identifiers of aggregates (as ReST representations do), and possibly a snapshot of relevant data (cf. Fowler)
# 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
) { }
id was already processedObjects aimed at creating other objects
Factories encapsulate the creation logic for complex objects
They ease the enforcement of invariants
They support dynamic selection of the most adequate implementation
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
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
POST on a collection resource is the Web counterpart of a factory
201 Created, plus Location: /customers/{id} header, plus the representation of the new entityPUT /customers/{id} lets the client choose the identifier (sensible only for natural keys)Different factory methods $\rightarrow$ different request schemas for the same collection
newCustomerPerson vs. newCustomerCompany $\rightarrow$ oneOf + discriminator in OpenAPIRequest bodies are untrusted representations (aka DTOs, data transfer objects), not domain objects
400 / 422)POST is not idempotent: if the client retries after a timeout, duplicates may be created
Idempotency-Key header, the server creates the entity only once (cf. IETF draft)# 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 examplesOrderManagementService needs an OrderStore to save ordersnew SqlOrderStore() itself, it is bound to that implementationGoal: minimise dependencies among types, to make implementations interchangeable
String, LocalDate) are tolerated exceptionsInterfaces are separated from classes; the wiring between them is delayed and performed outside the classes using them
:domain vs. :storage)main method, or an application-level factorynew somewhere else: the client now depends on the factorypublic 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 ...
}
OrderStore, CustomerFactory, Ordernew for any dependency $\rightarrow$ the implementation choice is somebody else’s businesspublic 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();
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);
}
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));
}
SqlOrderStore on a throw-away DB (e.g. via Testcontainers)java.time.Clock instead of calling LocalDateTime.now()| 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 |
@MockitoBean, Guice’s Modules.override(...)Bounded Context: enforce a model’s boundaries & make them explicit
Context Map: providing a global view on the domain and its contexts
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)
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
(source: Fowler)
Clearly identify & represent boundaries among contexts
Avoid responsibility diffusion over a single context
Avoid changes in the model for problems arising outside the context
Enforce context’s cohesion via automated unit and integration testing
As the domain evolves, the software model should evolve with it
Yet, the domain rarely changes as a whole
Contexts are bounded, but not isolated
Changes to a context and its model may propagate to other contexts / models
Domain / model changes are critical and should be done carefully
Preserve the integrity of the model w.r.t. the domain
Minimise the potential impact / reach of changes
Each relation among 2 contexts usually involves 2 ends/roles:
Integration among contexts $\leftrightarrow$ interaction among teams
*trust $\approx$ willingness to collaborate + seek stability
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
Keeping the kernel as small as possible is fundamental
Best when:
Key idea:
Customers may ask for features, suppliers will do their best to provide them
Suppliers shall warn before changing their model
Best when:
Key idea: downstream must conform to the upstream, reactively
Best when:
If upstream cannot be trusted, and interaction is pointless…
… downstream must defend itself against unexpected / unanticipated change
The upstream’s model is then reverse engineered & adapted
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
Domain layer: contains the domain model (entities, values, events, aggregates, etc.)
Application layer: contains services providing business logic
Presentation layer: provides conversion facilities to/from representation formats
Storage layer: supports persistent storage/retrieval of domain data
Interface layers (e.g. ReST API, MOM, View): let external entities access the software
Layering may be enforced in the code
By mapping layers into modules
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)A pattern where domain events are reified into time-stamped data and the whole evolution of a system is persistently stored
(cf. Fowler, Event Sourcing)
Historical data can be analysed, to serve several purposes
Past situations can be replayed
Enables complex event detection & reaction
Enables CQRS (described later)
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:
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)
Write model (a.k.a. command model)
(source: Fowler)
Whenever users want to perform an action in the system:
Whenever users want to inspect/observe the system at time $t$:
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
Several, non-mutually-exclusive strategies:
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
A simple domain keeping track of: customers, products, and orders.
Customers can either be companies or persons
Companies are identified by a VAT number
Persons are identified by tax codes
In any case a name and email are recorded for each customer
Both the name and email may vary over time
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
Money is represented by a decimal value and a currency
Currencies are represented by a name, a symbol and an acronym
Exchange rates keep track of the conversion rate
Information about exchange rates can be obtained from the Internet
We can compute the price of each product, in any currency, at any moment
Orders are identified by a serial number
They keep track of the products ordered by a customer
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
domain or application modulesCounterlong number, initially set to 0VariationIn practice:
CounterReader and CounterWriter interfacesVery simple domain: Tables
Rows…String valuesFunctionalities for CSV import/export are missing and need to be implemented via third-party libraries
A ParkingLot lets cars enter and exit, by plate
The class works, but it is hard to test
ParkingLot
java.time.Clock)ParkingLot receive its dependencies via constructor injectionMain be the composition root