giovanni.ciatto@unibo.it
Compiled on: 2025-02-18 — printable version
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?
Software will represent a solution to a problem in some business domain
Words do not have meaning per se, but rather w.r.t. a domain
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
Context: a portion of the domain
Model: a reification of the domain in software
Ubiquitous Language: language used by domain experts and mirrored by the model
A well established sphere of knowledge, influence or activity
A portion of the domain with a clear boundary:
- relying on a sub-set of the concepts of the domain
- where words/names have a unique, precise meaning
- clearly distinguishable from other contexts
Set of software abstractions mapping relevant concepts of the domain
- A language structured around the domain model
- used by all people involved into 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 tracking
Model the software around the ubiquitous language
Domain
$\xrightarrow{modelling}$
Model
(continued)
Chose 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
Entity: objects with an identifier
Value Object: objects without identity
Aggregate Root: compound objects
Domain Event: objects modelling relevant event (notifications)
Service objects: providing stateless functionalities
Repository: objects providing storage facilities
Factory: objects creating other objects
Genus-differentia definition:
- genus: both can be used to model elementary concepts
- differentia: entities have an explicit identity, value objects are interchangeable
Seats in 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
A composite entity, aggregating related entities/value objects
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
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)
Components of an aggregate should not hold references to components of other aggregates
(notice the link between Order
and Buyer
implemented by letting the Order
hold a reference to the BuyerID
)
Objects 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
interface CustomerFactory { + VatNumber computeVatNumber(String name, String surname, Date birthDate, String birthPlace) .. + Customer newCustomerPerson(TaxCode code, String fullName, string email) + Customer newCustomerPerson(String name, String surname, Date birthDate, String birthPlace, String email) .. + Customer newCustomerCompany(VatNumber code, String fullName, String email) } note bottom of CustomerFactory
CustomerFactory -r-> VatNumber: creates CustomerFactory -u-> Customer: creates
Objects 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
CustomerRegistry “1” o–> “N” Customer: contains CustomerRegistry –> CustomerID: exploits
Functional objects encapsulating the business logic of the software
e.g. operations spanning through several entities, objects, aggregates, etc.
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
interface OrderManagementService { + void performOrder(Order order) }
interface Order { + OrderID getId() + Customer getCustomer() + void setCustomer(Customer customer) + Date getTimestamp() + void setTimestamp(Date timestamp) + Map<Product, long> Amounts getAmounts() }
interface OrderID
interface Customer
interface Product
interface OrderStore
Order “1” *-r- “1” OrderID Order “1” *-d- “1” Customer Order “1” *-u- “N” Product OrderStore “1” *– “N” Order
OrderManagementService ..> Order: handles OrderManagementService ..> OrderStore: updates
note bottom of OrderStore: repository note top of Order: entity note right of OrderID: value object note right of Product: entity note right of Customer: entity note top of OrderManagementService: service
OrderID -u[hidden]- Product OrderID -d[hidden]- Customer
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
[Teacher’s Suggestion]: prefer neutral names for event classes in the model
OrderEventArgs
instead of OrderPerformedEventArgs
OrderEvent
instead of OrderPerformedEvent
orderIssued
, orderConfirmed
, orderCancelled
, etc.interface OrderManagementService { + void performOrder(Order order) .. + void notifyOrderPerformed(OrderEventArgs event) }
interface OrderEventArgs { + OrderID getID() + CustomerID getCustomer() + Date getTimestamp() + Dictionary<ProductID, long> getAmounts() }
interface OrderID
interface CustomerID
interface ProductID
OrderEventArgs “1” *-u- “1” OrderID OrderEventArgs “1” *-r- “1” CustomerID OrderEventArgs “1” *-d- “N” ProductID
OrderEventArgs .. OrderManagementService
note left of OrderEventArgs: domain event note left of OrderID: value object note left of ProductID: value object note right of CustomerID: value object note right of OrderManagementService: service
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
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 context / 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 for 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 from unexpected / unanticipated change
The upstream’s model is then reverse engineered & adapted
DDD does not enforce a particular architecture
Any is fine as long the model is integer
Layered architectures are well suited to preserve models’ integrity
Here we focus on the hexagonal architecture, a particular case of layered architecture
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
A pattern where domain events are reified into time-stamped data and the whole evolution of a system is persistently stored
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 leverages upon event sourcing and layered architecture…
… to deliver reactive, eventual-consistent solutions where:
Split the domain and application layers to segregate read/write responsibilities
Read model (a.k.a. view or query model)
Write model (a.k.a. command model)
Whenever users are willing to perform an action into the system:
Whenever users are willing to inspect/observe the system at time $t$:
Reification: is the process of computing the state of the system at time $t$ by applying of commands recorded up to time $t$
If queries and commands are stored on different databases
Several, non-mutually-exclusive strategies:
Repository: https://github.com/unibo-spe/ddd-exercise, branch exercise
(solutions on branch master
)
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 attained 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 many 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
modulesCounter
long
number, initially set to 0
Variation
In practice:
CounterReader
and CounterWriter
interfacesVery simple domain: Table
s
Row
s…String
valuesFunctionalities for CSV import/export are missing and need to be implemented via third-party libraries