Tactical DDD for enterprise onboardingPart 2
Strategic context mapping
Use bounded contexts, context maps, and anti-corruption layers to keep onboarding, IAM, billing, and external enterprise systems from collapsing into one model.
Once the language is clear, the next question is ownership.
Enterprise onboarding touches many teams. Customer success owns lifecycle readiness. InfoSec owns identity federation. Finance owns entitlements and seat quotas. Infrastructure owns provisioning. Client IT owns its own Microsoft Entra ID tenant, including groups synchronized from on-prem Active Directory.
If one onboarding transaction imports every model and writes every table, the system is not integrated. It is tangled.
For Northstar Health Group, this matters immediately. Customer Success wants to mark Northstar US ready after the kickoff call. InfoSec says SAML metadata has not been validated and required Entra groups are not mapped. Finance says the signed order form covers 100 seats, not the 500 users imported from AD. Platform says Northstar EU must be provisioned against the EU data-plane cluster, not the US cluster. Those are not implementation details; they are context boundaries.
Context boundaries protect model ownership
The enterprise problem space
The tempting implementation is a single mega-service:
async function onboardEnterpriseCustomer(input: OnboardingInput) {
await billing.createTrial(input.account);
await iam.configureSaml(input.identityProvider);
await provisioning.seedTenant(input.tenant);
await db.onboarding.update({ status: "complete" });
}
It ships quickly. Then a billing SKU-mapping bug blocks identity setup. A SAML metadata schema change leaks into onboarding DTOs. A provisioning retry creates duplicate billing entitlements. Nobody can change anything without reading everything.
This is how a modular monolith becomes a big ball of mud.
Bounded contexts
Carve the domain by business capability and ownership:
| Context | Owner | Model language |
|---|---|---|
| Onboarding | Ops / Customer Success | lifecycle, milestones, readiness gates, owner handoffs |
| Identity & Access | InfoSec | IdP federation, SAML/OIDC, role mappings, authentication |
| Billing & Entitlements | Finance / Sales | subscription, seats, quotas, feature grants, contract SKU |
| Provisioning | Platform | data-plane cluster, tenant seed records, background jobs |
| Entra ID / AD ACL | Integration | Entra tenant, synchronized AD groups, SAML/OIDC claims |
These contexts can live in one deployable. The boundary is model ownership, not network topology.
Context map
The relationships should be explicit:
- Onboarding -> Billing: customer-supplier. Onboarding cannot mark a tenant production-ready until Billing approves the seat allocation against the signed order form.
- Onboarding -> IAM: customer-supplier. Onboarding waits for validated SAML metadata and required Entra group mappings.
- Onboarding -> Provisioning: customer-supplier. Onboarding waits for the correct data-plane cluster, tenant seed records, and default admin role setup.
- Onboarding -> Entra ID / AD: anti-corruption layer. External identity-provider shapes must not become internal domain objects.
The context map should affect imports, contracts, tests, and team review ownership.
Failure mode: boundary bypass
The dangerous shortcut is letting Onboarding write another context’s tables directly:
await db.billingEntitlement.create({
data: {
tenant_id: tenantId,
plan: "enterprise",
seats: 500,
},
});
That code bypasses Billing’s language. Is enterprise a contract SKU, a feature bundle, or a negotiated exception? Can Northstar US draw from Northstar Global’s expansion pool? Onboarding should not decide that by inserting a row.
The safer boundary is a command or public contract:
await billingClient.requestSeatAllocation({
globalOrganizationId,
regionalDivisionId,
tenantId,
requestedSeats: 100,
contractId,
});
Billing can approve, reject, or ask for Sales review in its own model.
Anti-corruption layer
External enterprise systems are especially dangerous because every client has a different vocabulary.
For Northstar, the concrete external system is Microsoft Entra ID. Northstar still manages many groups in on-prem Active Directory, synchronizes them into Entra ID, and sends group assignments during SAML/OIDC sign-in or user provisioning.
That payload may look like this:
{
"tid": "northstar-entra-tenant",
"oid": "user-9f14",
"groups": ["adg-nhg-us-claims-admin", "adg-nhg-us-claims-review"],
"department": "Claims Operations",
"country": "US"
}
Onboarding should not spread Entra object ids, AD group names, or client department strings through the domain. Translate at the edge:
type IdentityProfileMapping = {
regionalDivisionId: RegionalDivisionId;
residencyPolicy: ResidencyPolicy;
roleMapping: RoleMappingDraft;
};
function mapEntraProfile(payload: EntraProfilePayload): IdentityProfileMapping {
return {
regionalDivisionId: divisionFromGroupClaims(payload.groups),
residencyPolicy: residencyPolicyForCountry(payload.country),
roleMapping: mapGroups(payload.groups),
};
}
The ACL protects the internal language from Microsoft-specific ids and Northstar-specific group naming. The domain should talk about regional divisions, residency policies, and role mappings, not tid, oid, or adg-nhg-us-claims-admin.
TypeScript enforcement
Architecture diagrams are not enough. Enforce boundaries in the monorepo:
{
"compilerOptions": {
"paths": {
"@onboarding/*": ["src/onboarding/*"],
"@billing/public": ["src/billing/public-api.ts"],
"@iam/public": ["src/iam/public-api.ts"]
}
}
}
Then ban deep imports:
{
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": ["@billing/domain/*", "@iam/domain/*"]
}
]
}
}
Onboarding can depend on Billing’s public contract. It cannot reach into Billing’s aggregate and mutate entitlement state.
The takeaway
Clear boundaries prevent a single tenant’s onboarding error from spilling into billing, IAM, provisioning, and the rest of the application ecosystem.
Strategic DDD is not ceremony. It is how you stop departmental models from corrupting each other.
Principles to apply in your own work
- Draw the onboarding flow as contexts, not services.
- For every cross-context call, write whether it is customer-supplier, conformist, partnership, or anti-corruption.
- Replace deep imports with public contracts.
- Add import rules that make illegal dependencies fail in CI.
- Pick one external client payload and build an ACL mapper before it reaches the domain.