- JavaScript 64.5%
- HTML 24.3%
- CSS 8.5%
- Dockerfile 2.7%
| example-1-monolith | ||
| example-2-microservices | ||
| .gitignore | ||
| README.md | ||
University Domain Driven Design and Microservices
This is a learning-focused exa\mple example to help understand Domain Driven Design (DDD) and how it can lead to a clean microservices architecture.
The example uses a University Management System as the example domain and includes two runnable code examples:
- Example 1 (modular monolith): one API deployable, but internally split by bounded context, plus a separately deployed web client.
- Example 2 (microservices): bounded contexts deployed as services, with an API gateway (BFF) and a separately deployed web client.
Table of contents
- Quick orientation
- DDD basics (short and practical)
- Activity Defining Contexts
- University: bounded contexts (the 3-context task)
- What makes a bounded context a good boundary (language, rules, ownership)
- Context maps: upstream and downstream relationships
- Anti Corruption Layer (ACL): what it does and how it works
- Mappings: how they are actually used (with examples)
- How the code reflects the design (with code pointers for both examples)
- How to run and test the examples (high level)
- Exercises and challenges
- Mermaid tips
1. Overview
The scenario
You are architecting a University Management System. The entity list is intentionally small:
- Student, Lecturer, Course, Room, Grade, Tuition Fee
Real universities have many more concepts (Enrollment, Term, Transcript, Invoice, Payment, Holds). In this example, we keep the model small so you can see boundaries clearly.
What you should focus on
This example is not about perfect university modeling. It is about seeing how DDD concepts become code:
- How you discover boundaries using activities and language
- How you keep models separate across boundaries
- How you integrate safely (ACL) so contexts can evolve independently
- How modular monolith boundaries map cleanly to microservices
2. DDD basics (short and practical)
DDD (Domain Driven Design) is a way to design software around business meaning (the domain) rather than around technical structures (tables, frameworks).
Core terms used in this example
- Domain: the business problem space (university management).
- Ubiquitous Language: shared vocabulary used by domain experts and developers, and reflected in code.
- Bounded Context: a boundary where a domain model is consistent.
- Context Map: how bounded contexts relate and integrate (including upstream and downstream).
- Anti Corruption Layer (ACL): a translator that protects a downstream model from an upstream model.
Why teams use DDD
DDD helps when:
- The system has meaningful business rules (not just CRUD).
- The same word means different things in different parts of the business.
- You need multiple parts of the system to evolve independently.
Pros
- Clearer code and fewer misunderstandings
- Reduced coupling and better change isolation
- A practical path from monolith to microservices
Cons and trade-offs
- Learning curve and some up-front modeling work
- Requires ongoing colexampleoration with domain experts
- Can be too heavy for simple CRUD-only systems
3. What are Activity Defining Contexts?
Activity Defining Contexts are a discovery technique: you find boundaries by starting from business activities and workflows, not from nouns or database tables.
Ask:
- What activity is happening?
- What rules apply during that activity?
- Do key terms change meaning in another activity?
If the language or rules change across activities, you likely need separate bounded contexts.
University activities you can use immediately:
- Teaching and Scheduling
- Assessment and Student Records
- Billing and Finance
4. University task: group entities into 3 bounded contexts
Entities:
- Student, Lecturer, Course, Room, Grade, Tuition Fee
A clear split into 3 logical bounded contexts:
-
Teaching and Scheduling
- Course, Lecturer, Room
-
Student Records and Assessment
- Student, Grade
-
Finance
- Tuition Fee
Important modeling note:
- The same real-world concept can appear in multiple contexts, but each context owns its model. Cross context links should use stable identifiers (for example
studentId), not shared domain classes.
5. What makes a bounded context a good boundary
A bounded context is a good boundary when it has:
- consistent language
- consistent rules
- a natural ownership boundary
The next three sections illustrate each signal with diagrams and concrete examples.
5.1 Consistent language
Within a bounded context, terms should mean one thing. Across contexts, the same word can legitimately mean different things.
Example: Student
- In Records: Student means an academic identity and transcript owner.
- In Finance: Student means an account holder and payer identity (billing).
Example: Course
- In Scheduling: Course means what is delivered (and often includes offerings and timetable concepts).
- In Records: Course may only be a reference used for grade attachment and transcript.
flowchart LR
REC["Records context"] --> S1["Student means<br/>academic identity and transcript owner"]
FIN["Finance context"] --> S2["Student means<br/>payer identity and account holder"]
SCH["Scheduling context"] --> C1["Course means<br/>catalog and delivery details"]
REC --> C2["Course means<br/>reference for grading"]
Practical rule:
- Avoid sharing domain classes across contexts. Share identifiers and translate.
5.2 Consistent rules
Rules tend to cluster by context. If you find yourself mixing rules from different clusters, that is often a sign your boundary is wrong.
Scheduling rules (delivery constraints):
- A room cannot host two classes at the same time.
- A lecturer cannot teach two classes at the same time.
Records rules (assessment integrity):
- Grade must be valid (e.g. A to F).
- Grade must attach to exactly one student and one class reference.
Finance rules (billing policy):
- Invoice amount must be non-negative.
- Paid invoice cannot be modified (typical policy).
flowchart TB
SCH["Scheduling context"] --> R1["Rules<br/>timetable constraints"]
REC["Records context"] --> R2["Rules<br/>grading and transcript integrity"]
FIN["Finance context"] --> R3["Rules<br/>invoices and payment policy"]
Practical rule:
- Keep each rule close to the model that owns it. Do not enforce scheduling constraints inside the finance model, for example.
5.3 Natural ownership boundary
A bounded context is easier to maintain when it aligns with ownership. Ownership can be a team, a department, or simply a clear responsibility boundary.
In a university, ownership often aligns like this:
- Scheduling office owns timetables and resource allocation.
- Registry owns transcripts and academic outcomes.
- Finance office owns billing and payments.
flowchart LR
T1["Scheduling team"] --> SCH["Scheduling context"]
T2["Registry team"] --> REC["Records context"]
T3["Finance team"] --> FIN["Finance context"]
Practical rule:
- Even if you have one team today, designing with ownership boundaries helps future growth and reduces coupling.
6. Context maps: upstream and downstream relationships
A Context Map documents how bounded contexts integrate and who leads at each boundary.
6.1 Upstream and downstream (definitions)
Upstream context:
- The provider
- Owns the concept and publishes a contract (API or events)
- Often the source of truth for a capability
Downstream context:
- The consumer
- Depends on the upstream contract to complete use cases
- Should translate upstream concepts into downstream concepts (often via an ACL)
Simple mental model:
- Upstream: I define it and publish it.
- Downstream: I need it and consume it.
6.2 Why upstream and downstream matters
- It clarifies who owns the contract and who must adapt to change.
- It guides where translation should live (usually downstream).
- It helps you avoid accidental shared models.
6.3 University context map
Relationships used in this example:
- Identity is upstream of all contexts (tokens and claims).
- Scheduling is upstream of Records (Records validates class references).
- Records and Finance relationship depends on billing policy; we show an optional events arrow.
flowchart LR
ID["Identity<br/>upstream"] --> SCH["Scheduling<br/>downstream"]
ID --> REC["Records<br/>downstream"]
ID --> FIN["Finance<br/>downstream"]
SCH -->|"contract: class lookup"| REC
REC -->|"optional events"| FIN
Practical tip:
- A context can be upstream for one concept and downstream for another. The relationship is specific to the integration boundary.
7. Anti Corruption Layer (ACL): what it does and how it works
An ACL is a translation layer owned by the downstream context that protects its model from upstream model leakage and change coupling.
In this example:
- Records is downstream of Scheduling for class information.
- Records must not import Scheduling domain classes.
- Records calls Scheduling and translates the response into a Records-owned value:
ClassRef.
7.1 The problem ACL solves
Without an ACL, teams commonly fall into these traps:
- Shared domain library: one
CourseorScheduledClassclass used everywhere. - Shared database: services read each other’s tables directly.
- Leaky dependencies: Records starts depending on room scheduling fields.
These increase coupling and make microservice extraction difficult.
7.2 ACL example (what it looks like)
Records stores a simplified ClassRef instead of Scheduling’s full ScheduledClass representation.
Scheduling response example:
{
"classId": "class-1",
"courseId": "c-1",
"lecturerId": "lecturer-456",
"roomId": "room-9",
"timeslot": "Mon 10:00"
}
Records keeps only what it needs:
{
"classId": "class-1",
"courseId": "c-1",
"timeslot": "Mon 10:00"
}
flowchart LR
REC["Records context"] --> ACL["ACL scheduling client<br/>returns ClassRef"]
ACL --> SCH["Scheduling context or service"]
7.3 What issues ACL resolves (concrete examples)
- Model leakage
- If Records imports Scheduling’s
ScheduledClass, Records will gain access toroomIdandlecturerIdeven if it should not care about them. - Over time, Records accumulates scheduling logic, and the boundary collapses.
ACL prevents this by returning a Records-owned ClassRef with only the fields Records needs.
- Change coupling
- Scheduling may rename fields, split a concept, or add new fields.
- Without an ACL, downstream code breaks everywhere.
With an ACL, downstream changes are localized to the translator. The downstream domain model remains stable.
- Extraction readiness
- In the monolith, ACL can call Scheduling in-process.
- In microservices, ACL can call Scheduling over HTTP.
The domain model stays the same; only the ACL adapter changes.
7.4 Failure modes to consider
When Records depends on Scheduling, consider these cases:
- Scheduling returns 404 (unknown classId).
- Scheduling is slow or unavaiexamplele (timeouts).
- Scheduling contract changes (response shape changes).
In production, you would add patterns like timeouts, retries, circuit breakers, and contract versioning. The example keeps this minimal but the ACL is where these concerns naturally belong.
8. Mappings: how they are actually used
DDD uses mapping in a few common ways. In this example you will see three practical mappings.
8.1 Strategic mapping: context map
This is the context map: upstream/downstream relationships and integration boundaries.
You use it to decide:
- which context adapts to which
- where to place ACLs
- and which contracts need to be stable
8.2 Contract mapping: API shapes are contracts
Scheduling publishes a class lookup contract:
GET /classes/:classIdreturns a shape that consumers rely on
Records depends on this contract. If Scheduling changes it, you must coordinate or version the API.
8.3 Translation mapping: ACL mapping
This is the code mapping you implement:
- Scheduling payload -> Records
ClassRef
This mapping is where you remove unwanted fields and reshape data into downstream terminology.
9. How the code reflects the design (with pointers for both examples)
This section helps you connect the concepts to concrete code. Path names below assume you are in the repository root.
9.1 Example 1 (modular monolith): where things live
Example 1 is a single API deployable with context modules.
Bounded contexts:
- Scheduling:
example-1-monolith/apps/api/src/contexts/scheduling/ - Records:
example-1-monolith/apps/api/src/contexts/records/ - Finance:
example-1-monolith/apps/api/src/contexts/finance/
Identity module (token issuance and auth):
example-1-monolith/apps/api/src/identity/
Key design-to-code links:
-
Domain rules and invariants are expressed in domain models:
- Scheduling domain:
.../scheduling/domain/* - Records domain:
.../records/domain/* - Finance domain:
.../finance/domain/*
- Scheduling domain:
-
Upstream and downstream integration is explicit via an ACL adapter:
- Records in-process ACL adapter:
example-1-monolith/apps/api/src/contexts/records/acl/inProcessSchedulingClient.js
- Records in-process ACL adapter:
-
The web client is separately deployed to make UI avaiexampleility explicit:
- Example 1 web client:
example-1-monolith/apps/web/
- Example 1 web client:
9.2 Example 2 (microservices): where things live
Example 2 deploys each bounded context as a service plus a gateway.
Services:
- Identity service:
example-2-microservices/apps/identity-service/src/ - Scheduling service:
example-2-microservices/apps/scheduling-service/src/ - Records service:
example-2-microservices/apps/records-service/src/ - Finance service:
example-2-microservices/apps/finance-service/src/ - API gateway (BFF):
example-2-microservices/apps/api-gateway/src/ - Web client (nginx):
example-2-microservices/apps/web/
Key design-to-code links:
-
Upstream contracts are published by upstream services:
- Scheduling publishes
GET /classes/:classIdinexample-2-microservices/apps/scheduling-service/src/app.js
- Scheduling publishes
-
Downstream protection is done in the downstream service via ACL:
- Records ACL translator:
example-2-microservices/apps/records-service/src/schedulingAcl.js - Records use case calls the ACL when recording a grade:
example-2-microservices/apps/records-service/src/app.js
- Records ACL translator:
-
The gateway is the single backend entrypoint for the browser and proxies to services:
- Gateway routing:
example-2-microservices/apps/api-gateway/src/app.js
- Gateway routing:
10. How to run and test (high level)
Each example is independent. Run one at a time.
Example 1:
- Start:
docker compose up --buildinsideexample-1-monolith/ - UI:
http://localhost:8080
Example 2:
- Start:
docker compose up --buildinsideexample-2-microservices/ - UI:
http://localhost:8081
Use the UI buttons to log in as admin, lecturer, or student and perform basic flows:
- Create course and scheduled class (admin)
- Create student (admin)
- Record grade (lecturer)
- View grades and invoices (student)
11. Exercises and challenges
These are designed to test understanding and build confidence.
Exercise 1: Ubiquitous language per context
Write 8 to 12 terms and definitions for each context:
- Scheduling context terms
- Records context terms
- Finance context terms
Checkpoint:
- Identify at least one term that changes meaning between contexts.
Exercise 2: Rules by context
Identify 3 rules per context and decide where they should live:
- domain code (entity/value object invariants)
- application code (use case orchestration)
- cross-context policy (integration or process manager)
Exercise 3: ACL behavior and failure modes
Answer these in writing (or implement if you want to extend the example):
- What should Records do if Scheduling returns 404 for a classId?
- What should Records do if Scheduling is down?
- Which layer should handle retries and timeouts, and why?
Exercise 4: Upstream and downstream reasoning
For each integration, explain upstream and downstream:
- Identity to Records
- Scheduling to Records
- Records to Finance (choose an approach and justify it)
Challenge 1: Add Enrollment
Add an Enrollment concept (conceptual or code):
- Student enrolls in classId
- Grade can only be recorded if Enrollment exists
Decide which context owns Enrollment and justify your decision using language and rules.
Challenge 2: Event-driven integration (advanced)
Replace synchronous class lookup with events:
- Scheduling emits ClassScheduled
- Records stores a local projection of class references
- Grade recording uses the projection instead of calling Scheduling
Describe the consistency trade-offs and failure modes.
Appendix 1: The Book example (separate illustration of the shared-model trap)
A term can mean different things in different contexts:
- Sales: Book means title, author, price, cover image.
- Warehouse: Book means weight, dimensions, aisle location, barcode.
Microservice mistake: sharing one Book class across both contexts.
DDD solution: separate bounded contexts and separate models, integrate using ISBN only.
flowchart LR
SALES["Sales context"] --> B1["Book model<br/>title author price"]
WH["Warehouse context"] --> B2["Book model<br/>weight dimensions barcode"]
SALES <-->|"integrate by ISBN"| WH
Appendix 2: Reading
Books
- Domain-Driven Design Distilled (Vaughn Vernon) https://www.informit.com/store/domain-driven-design-distilled-9780134434421 Free sample pages (PDF): https://ptgmedia.pearsoncmg.com/images/9780134434421/samplepages/9780134434421.pdf
- Domain-Driven Design: Tackling Complexity in the Heart of Software (Eric Evans) Overview: https://www.domainlanguage.com/ddd/blue-book/ DDD Reference (free PDF, very useful): https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf
- Monolith to Microservices (Sam Newman) O’Reilly page: https://www.oreilly.com/library/view/monolith-to-microservices/9781492047834/
- Building Microservices, 2nd Edition (Sam Newman) Author page (includes a free chapter): https://samnewman.io/books/building_microservices_2nd_edition/ O’Reilly page: https://www.oreilly.com/library/view/building-microservices-2nd/9781492034018/
Online resources
DDD (strategic design, bounded contexts, context mapping)
- DDD Reference (Eric Evans) - free PDF https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf
- Domain Language (Eric Evans site) - Blue Book page and DDD resources https://www.domainlanguage.com/ddd/blue-book/
- Martin Fowler - Microservices (good overview and links) https://martinfowler.com/articles/microservices.html
- Martin Fowler - Bounded Context https://martinfowler.com/bliki/BoundedContext.html
- Martin Fowler - Context Map https://martinfowler.com/bliki/ContextMap.html
- Martin Fowler - Domain Model https://martinfowler.com/eaaCatalog/domainModel.html
Microservices architecture
- Microsoft Azure Architecture Center - Microservices architecture style https://learn.microsoft.com/azure/architecture/guide/architecture-styles/microservices
- Microsoft Learn - Microservices (entry points and learning paths) https://learn.microsoft.com/training/browse/?terms=microservices
Migration patterns (monolith to microservices)
- Martin Fowler - Strangler Fig Application https://martinfowler.com/bliki/StranglerFigApplication.html
- Martin Fowler - Branch by Abstraction https://martinfowler.com/bliki/BranchByAbstraction.html
Contracts, integration, and testing (very relevant once you split services)
- Martin Fowler - Consumer-Driven Contracts https://martinfowler.com/articles/consumerDrivenContracts.html
- Martin Fowler - Tolerant Reader (backward compatibility mindset) https://martinfowler.com/bliki/TolerantReader.html
Reliability basics for distributed systems (microservices reality)
- Google SRE book (free online) - selected chapters are especially useful https://sre.google/sre-book/table-of-contents/
Enda Lee 2026