Tactical DDD for enterprise onboardingPart 6
Bringing the onboarding engine together
Connect the DDD pieces into one pragmatic enterprise onboarding architecture: language, boundaries, aggregates, workflows, isolation, tests, and operational tradeoffs.
The point of this series is not to use every DDD pattern.
The point is to make better architecture decisions in a domain where mistakes are expensive: enterprise onboarding for a shared-schema SaaS product.
Northstar Health Group gave us a realistic pressure test. One global customer has regional compliance boundaries, operational tenants, Entra ID federation, commercial quotas, regional data-plane provisioning, and isolation risk. That is enough complexity to justify careful modeling, but not enough to justify turning the whole system into a pattern museum.
DDD patterns should line up with business risk
Where the code lives
The series does not require microservices.
A good first implementation can be a modular monolith:
- one deployable web application for HTTP/admin/API requests;
- one worker process running the outbox dispatcher and onboarding saga handlers;
- one shared application database with tenant tables, aggregate snapshots, and an outbox table;
- internal modules for Onboarding, Billing, IAM, Provisioning, and the Entra/AD anti-corruption layer;
- public module APIs and import rules that prevent one context from reaching into another context’s internals.
One deployment can still have hard domain boundaries
That shape is often the right place to start. It keeps local development, transactions, observability, and refactoring simple while the domain language is still evolving.
The worker is not a separate domain. It is another runtime entry point into the same application model. HTTP handlers accept requests. Workers react to durable facts. Both should call application services and domain objects through the same boundaries.
When topology matters
Deployment topology matters when it changes ownership, reliability, or operational risk. It matters less when it is only a diagram preference.
| Situation | Reasonable shape |
|---|---|
| One product team owns onboarding end to end | Modular monolith with strict modules and worker processes |
| Billing, IAM, and Platform are owned by separate stream-aligned teams | Separate services or packages with published contracts |
| Provisioning has different scaling or failure behavior | Dedicated worker pool or service for data-plane provisioning |
| Regulatory controls require separate deployment or audit boundaries | Separate deployable with explicit API and audit trail |
| The team is still discovering the domain language | Keep deployment simple and enforce boundaries in code first |
DDD helps either way. In a monolith, it gives you module boundaries, public contracts, and language ownership. In a distributed system, it tells you which service contracts are meaningful and which integrations need anti-corruption layers.
Do not split services because the diagram has boxes. Split when the context boundary already exists in the business and the operational cost is worth paying.
The architecture in one pass
- Start with business language. Separate global organization, regional division, tenant, contract, residency policy, and identity readiness.
- Draw context boundaries. Onboarding owns lifecycle readiness. Billing owns commercial allocation. IAM owns Entra/SAML federation. Platform owns regional data-plane provisioning.
- Protect invariants in aggregates.
TenantConfigurationdecides whether a configuration change is valid after the application layer loads contract, residency, and lifecycle facts. - Coordinate time with events and sagas. The outbox makes state changes publishable. The saga handles long-running work, retries, blocked states, and compensation.
- Make tenant isolation a system property. Repositories, execution context, query filters, database constraints, and audit logs work together.
Each move should answer a business question. If it does not, skip it.
What belongs where
| Concern | Belongs in | Why |
|---|---|---|
| JSON shape, required fields, enum parsing | HTTP adapter / command parser | It protects the application from malformed input |
| Contract quota, residency, lifecycle transitions | Aggregate or domain service | It protects business invariants |
| Fetching contract and residency facts | Application service | It coordinates I/O before calling the domain |
| Cross-context approval | Public context contract | It prevents model ownership leaks |
| Long-running provisioning | Saga / process manager | It makes time, retries, and compensation explicit |
| Tenant query filtering | Infrastructure and database | It makes isolation the default |
That separation is what keeps DDD pragmatic. The aggregate does not fetch data. The controller does not decide contract policy. The saga does not become a god object. The repository does not hide business rules.
A realistic request path
Here is the final shape for a Northstar seat allocation request:
- The HTTP adapter validates request shape and parses command fields.
- The application service loads
TenantConfigurationandEnterpriseContract. - The aggregate evaluates
allocateSeats(requested, contract). - Domain errors return as domain errors, not HTTP exceptions.
- The adapter maps domain errors to HTTP problem responses.
- On success, the repository saves the new tenant configuration.
- The same transaction writes an outbox event.
- A dispatcher publishes the event.
- The onboarding saga advances or blocks the workflow.
- Tenant-scoped reads and writes run through context-aware repositories.
There is no single magic layer. The design works because each layer has a narrow job.
Where teams overuse DDD
Do not create aggregates for static lookup tables.
Do not wrap every string in a value object before the language is stable.
Do not introduce a saga for work that completes inside one local transaction.
Do not split bounded contexts by database table or folder name.
Do not use repositories as a blanket excuse to hide every query.
DDD is most useful where the business language is subtle, the invariants matter, and the cost of getting the model wrong is high. For simple CRUD, use simple CRUD.
What to test
Test the places where the design carries risk:
- context mapping tests that prevent illegal imports;
- aggregate tests for quota, residency, lifecycle, and security invariants;
- adapter tests that map domain errors to HTTP responses;
- saga tests for retryable failure, blocked states, and compensation;
- repository/infrastructure tests proving tenant filters are injected;
- migration tests for tenant-scoped indexes and foreign keys.
The tests should not all mention Northstar. Northstar is the teaching scenario. The tests should name reusable business rules.
Final checklist
Before applying DDD to an enterprise workflow, ask:
- What business language is currently ambiguous?
- Which ambiguity has caused or could cause real harm?
- Which teams own the concepts involved?
- Which invariants must be protected before persistence?
- Which decisions require facts loaded from other stores or contexts?
- Which workflow steps can fail after the first transaction commits?
- Which compensations are required after partial success?
- Which tenant isolation guarantees should be enforced below application code?
- Which patterns solve the actual problem, and which ones are ceremony?
If the answers are clear, DDD gives you a vocabulary and a set of tools. If the answers are not clear, start with more domain discovery, not more abstractions.