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

Context boundaries protect model ownershiprequest allocationwait for readinessraw payloaddomain mappingOnboardingreadiness lifecycleBillingcontracts + seatsIAMfederationEntra / AD ACLtranslation boundaryNorthstar Entra IDAD group claims
Onboarding coordinates the lifecycle, but IAM, billing, and external client systems keep their own language and contracts.

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:

ContextOwnerModel language
OnboardingOps / Customer Successlifecycle, milestones, readiness gates, owner handoffs
Identity & AccessInfoSecIdP federation, SAML/OIDC, role mappings, authentication
Billing & EntitlementsFinance / Salessubscription, seats, quotas, feature grants, contract SKU
ProvisioningPlatformdata-plane cluster, tenant seed records, background jobs
Entra ID / AD ACLIntegrationEntra 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

  1. Draw the onboarding flow as contexts, not services.
  2. For every cross-context call, write whether it is customer-supplier, conformist, partnership, or anti-corruption.
  3. Replace deep imports with public contracts.
  4. Add import rules that make illegal dependencies fail in CI.
  5. Pick one external client payload and build an ACL mapper before it reaches the domain.

Next: Immutability in aggregates and value objects.