DDD & Bounded Contexts
:::info Source
Sourced from docs/02-ddd-bounded-contexts.md in the documentation repo.
:::
Companion: 01 Enterprise Architecture · 03 Microservices · 04 Event-Driven Architecture · 12 Data Models · 13 Security & Tenancy
This document is the authoritative DDD view of Ghasi-edTech. It enumerates bounded contexts, aggregates with codified invariants, ACL translators, value objects allowed to cross context boundaries, the conflict-resolution matrix, and the ubiquitous language per context. All other documents defer to this one for strategic domain definitions.
1. Strategic DDD
1.1 Domain Classification
| Class | Contexts | Rationale |
|---|---|---|
| Core (competitive differentiator) | Authoring, Delivery, Progress (LRS), Assignment, Marketplace, AI Services, Offline Sync | Where Ghasi-edTech earns its keep |
| Supporting (specific, not differentiating) | Catalog, Content-Packaging, Assessment, Certification, Enrollment, Search, Analytics, Compliance & audit | Necessary but pattern-driven |
| Generic (commodity) | Identity, Tenant, Billing, Notification, Media | Could be replaced by SaaS, kept in-house for isolation + cost |
1.2 Context List (20 logical contexts)
- Identity & Access
- Tenant
- Catalog
- Authoring
- Content-Packaging
- Marketplace
- Billing
- Enrollment
- Assignment
- Delivery
- Progress (LRS)
- Assessment
- Certification
- Notification
- Media
- Search
- Analytics
- AI Services
- Offline Sync
- Compliance & audit
system.mdoriginally grouped 18 contexts treating Sync as platform-infra. Sync is listed as its own context for ownership clarity; Compliance & audit is the bounded context behind compliance-service (GDPR DSR orchestration, audit coordination). Totals across other docs should align with 03 Microservices (20 logical services) unless a document explicitly scopes a subset.
2. Context Map (Relationship Patterns)
Every cross-context relationship is one of: Shared Kernel (SK), Customer/Supplier (CS), Conformist (CF), Anti-Corruption Layer (ACL), Open Host Service (OHS), Published Language (PL), or Partnership (PA). No context has "direct dependency" that isn't one of these.
| Upstream → Downstream | Pattern | Notes |
|---|---|---|
| Tenant → every context | SK on TenantId value object only | See §5 for full list of crossable VOs. No shared entities. |
| Identity → every context | CF for JWT claims | Identity issues JWT; consumers conform to claim contract. |
| Authoring → Content-Packaging | CS + ACL | Authoring emits authoring.course_draft.published.v1; Content-Packaging translates authoring's block tree into a PlayPackage (different model). |
| Content-Packaging → Delivery | PL (PlayPackage schema) | PlayPackage is the published, versioned contract. |
| Content-Packaging → Catalog | CS | Catalog creates a CourseVersion aggregate on content.play_package.built.v1. |
| Marketplace → Billing | CS | Marketplace orchestrates payment via billing events. |
| Marketplace → Enrollment | CS via saga | Purchase saga → marketplace.license.granted.v1 → Enrollment creates Enrollment. |
| Assignment → Enrollment | CS | Compliance window → Enrollment spawned on first play. |
| Delivery → Progress | CS | Delivery emits intents; Progress is authoritative LRS. |
| Delivery → Assessment | CS | Player delegates quiz serving + scoring. |
| Progress → Certification | CS | Completion event triggers certificate issuance. |
| AI Services → every content/learning context | OHS + PL | Gateway publishes one stable AIClient port; all AI artifacts carry a shared AIProvenance VO. |
| Offline Sync → every replicable context | OHS + CF | Sync publishes one protocol; every replicable context conforms. |
| Tenant → Compliance & audit | CS | Tenant-scoped data residency and policy flags feed DSR orchestration. |
| Compliance & audit → Notification | CS | Subject-request lifecycle notifications (export ready, erasure status, etc.). |
| Analytics → Compliance & audit | CS | Audit exports and Merkle anchoring handoff (see EP-21). |
| Compliance & audit → AI Services | CS | Compliance review of prompts, completions, and provenance records (admin paths). |
| External IdPs → Identity | ACL | OIDC/SAML normalized into internal VOs. |
| External LLMs → AI Services | ACL | Vendor responses normalized into AICompletion, Embedding, AIArtifact. |
| External Payment Gateways → Billing | ACL | Vendor tokens normalized into Payment, Invoice, Payout. |
| External SCORM Sources → Content-Packaging | ACL | 3rd-party zips normalized into PlayPackage projection. |
2.1 Visual Context Map
┌─────────────┐
│ Tenant │ (SK: TenantId VO)
└──────┬──────┘
│
┌──────────────┐ │ ┌──────────────┐
│ Identity │────┴────│ Notification│
│ (CF via JWT)│ └──────────────┘
└──────┬───────┘
│
┌──────────┴─────────┐
│ │
┌────▼────┐ ┌─────▼─────┐ ┌──────────────┐
│ Catalog │◄────────┤ Authoring │◄────────│ Content-Pkg │
│ (CS) │ (CS) │ (Core) │ (ACL) │ (PL) │
└────┬────┘ └─────┬─────┘ └──────┬───────┘
│ │ │
┌────▼─────────┐ ┌─────▼─────┐ │
│ Marketplace │ │ AI Svc │◄── OHS: consumed by every authoring/learner context
│ (Core saga) │ │ (OHS+PL) │
└────┬─────────┘ └─────┬─────┘
│ │
┌────▼────┐ ┌─────▼─────┐
│ Billing │ │ Assessment│
│ (ACL) │ │ (CS) │
└────┬────┘ └─────┬─────┘
│ │
┌────▼────────┐ ┌─────▼─────┐ ┌──────────────┐
│ Enrollment │◄────┤ Assignment│ │ Certification│
│ (CS) │ (CS)│ (Core) │ │ (CS) │
└────┬────────┘ └─────┬─────┘ └──────┬───────┘
│ │ │
└─────┬──────────────┘ │
│ │
┌─────▼─────┐ ┌──────────┐ ┌──────▼──────┐
│ Delivery │───►│ Progress │───►│ Analytics │
│ (Core) │(CS)│ (LRS) │(CS)│ (CS) │
└─────┬─────┘ └──────────┘ └─────────────┘
│
┌─────▼─────┐ ┌──────────┐ ┌──────────────┐
│ Media │ │ Search │ │ Offline Sync │ (OHS cross-cutting)
└───────────┘ └──────────┘ └──────────────┘
3. Ubiquitous Language — Named Terms Per Context
To prevent linguistic collisions, every context defines its own spelling of shared concepts. The tables below include (a) terms unique to a context and (b) explicit disambiguation for shared names.
3.1 Identity & Access
Unique: User, Account, Session, Credential, Device, TrustedDevice, MFAFactor, APIKey, ServiceAccount, IdentityProvider, Claim, PasswordPolicy. Disambiguation: "User" in Identity refers strictly to the authentication subject — not the organizational person. Organizational membership lives in Tenant.
3.2 Tenant
Unique: Tenant (root), TenantType, OrgUnit, Role, Membership, TenantSettings, DataResidency, Plan, FeatureFlagOverride, DynamicGroup. Disambiguation: "Role" here is a tenant-scoped RBAC aggregate, not an Identity concept.
3.3 Catalog
Unique: Course (published-only form), CourseVersion, Module, LessonRef, Taxonomy, Tag, Category, Visibility, Locale.
Disambiguation: Course in Catalog is immutable + published. It is not the same aggregate as CourseDraft in Authoring; they are linked by event (content.play_package.built.v1) and share only the CourseId VO.
3.4 Authoring
Unique: CourseDraft (root), ModuleDraft, LessonDraft, Block (discriminated union), MediaRef, InteractionDef, BranchingNode, DraftVersion, AIDraftBlock, CollaborationSession.
Disambiguation: CourseDraft is the mutable workspace aggregate. The published catalog Course is a different aggregate in a different context.
3.5 Content-Packaging
Unique: PlayPackage (root), PackageManifest, AssetReference, SCORMExport, HTMLExport, xAPIExport, PlayPackageBundle, LicenseEnvelope, PackageHash, PackageSignature.
Disambiguation: PlayPackage is the runtime projection — deliberately different shape from CourseDraft.
3.6 Marketplace
Unique: Listing (root), PricingPlan, License, Order, OrderLine, PaymentIntent, Refund, PayoutSchedule, ProviderEarnings, CouponCode, MarketplaceVisibility.
3.7 Billing
Unique: Subscription (root), Invoice, Payment, Customer, TaxJurisdiction, DunningProcess, Payout, PayoutBatch.
3.8 Enrollment
Unique: Enrollment (root), Seat, EnrollmentState, CourseVersionRef. Disambiguation: An Enrollment is per learner per course-version. Licenses (Marketplace) and Assignments (Assignment) are upstream causes, never sub-entities here.
3.9 Assignment
Unique: Assignment (root), AssignmentTarget, RecurrenceRule (RRULE), DueDate, GracePeriod, EscalationPolicy, ComplianceWindow.
3.10 Delivery
Unique: PlaySession (root), NavigationCursor, PlayState, AssistantTurn, OfflineMount.
3.11 Progress (LRS)
Unique: Statement, Attempt, InteractionResult, CompletionRecord, PassFailRecord, Activity, Verb, Actor.
Disambiguation: Actor here is an xAPI actor projection — not User (Identity) nor Membership (Tenant).
3.12 Assessment
Unique: QuizBank, Question, BranchingScenario (root), ScenarioNode, DecisionEdge, AttemptResult, GradingRule.
3.13 Certification
Unique: Certificate (root), CertificateTemplate, IssuanceProof, RevocationRecord, VerificationToken, OfflineIssuanceClaim.
3.14 Notification
Unique: Notification (root), DeliveryChannel, Template, Preference, Digest, DeliveryAttempt, BounceRecord.
3.15 Media
Unique: MediaAsset (root), AssetVariant, TranscodeJob, CaptionTrack, SubtitleTrack, AIMediaArtifact.
3.16 Search
Unique: SearchableDocument, IndexAlias, SemanticEmbedding, Recommendation, Ranker.
3.17 Analytics
Unique: Event, MetricDefinition, Dashboard, Report, ExportJob, CohortDefinition.
3.18 AI Services
Unique: Prompt (root), PromptVersion, Model, AICompletion, Embedding, AIArtifact, SafetyVerdict, AIBudget, AIAuditEntry, PromptInjectionSignal.
3.19 Offline Sync
Unique: SyncRegistration, SyncCursor, LocalMutation, RemoteDelta, VectorClock, ConflictRecord, DeviceBinding.
3.20 Compliance & audit
Unique: SubjectRequest (export/erasure), GdprDataExportRequest, ErasureRequest (planned), LegalHold (planned), BundleManifest, AuditAnchorRef (planned), ComplianceReviewSession (planned). Disambiguation: ComplianceWindow (§3.9 Assignment) is assignment scheduling policy — not this context. Compliance & audit here means regulatory data-subject rights orchestration and platform audit coordination owned by compliance-service (03 — compliance-service).
4. Aggregates — Codified Invariants
Every aggregate root below is stated with: Identity, Ownership, State Machine, Invariants (enforced in the domain layer), and Cross-Aggregate Rules (enforced by application layer or saga).
4.1 CourseDraft (Authoring)
- Identity:
CourseDraftId. - Ownership:
TenantId,authors: UserId[]. - State:
editing → in_review → approved → publishing → published_idle(terminal-idle returns toeditingon fork). - Invariants (domain):
TenantIdidentical across every child (module, lesson, block). Cross-tenant reference throwsDomainError.CrossTenant.- Every
Blockbelongs to exactly one lesson; ordering within a lesson is contiguous from 0. - AI-generated blocks persist only with
status='draft_ai'AND a non-nullaiProvenanceVO. Missing provenance throwsDomainError.AIProvenanceRequired. - Draft cannot transition
→ publishingunless everyrequired: trueblock hasstatus ∈ {'reviewed','published'}AND every media reference resolves. draftVersionis strictly monotonic.status='draft_ai'→ cannot berequired: true(AI must be reviewed before being required).
- Cross-Aggregate Rules (saga): publish saga coordinates Content-Packaging + Catalog; compensation returns draft to
approvedwith audit entry.
4.2 PlayPackage (Content-Packaging)
- Identity:
PlayPackageId. - Ownership:
TenantId,courseVersionId. - State:
building → built → revoked(no return tobuilding). - Invariants:
- Immutable after
built. - Hash matches
PackageSignature; signature verifies against tenant key. localewithin tenant-supported locales.
- Immutable after
- Cross-Aggregate: only Content-Packaging writes; Delivery reads via PlayPackage GET API.
4.3 PlayPackageBundle (Content-Packaging)
- Identity:
BundleId. - Ownership:
TenantId,playPackageId,enrollmentId,deviceId. - State:
available → revoked. - Invariants:
- Encryption key derived via HKDF from
(tenantKey, devicePubKey, bundleId). LicenseEnvelope.signatureverifies against tenant key; envelopeexpiresAtin future at issuance.- Bundle bound to single
deviceId; cross-device decryption cryptographically impossible.
- Encryption key derived via HKDF from
4.4 Listing (Marketplace)
- Identity:
ListingId. - Ownership:
providerTenantId,courseId. - State:
Draft → Submitted → Approved → Live → Suspended → Retired. - Invariants:
- Listing has ≥ 1 PricingPlan when
Submitted. - PricingPlan
kind+currency+pricerequired; negative price throws. - A Course may have at most one
LiveListing per provider tenant (uniqueness enforced at DB).
- Listing has ≥ 1 PricingPlan when
4.5 Order (Marketplace)
- Identity:
OrderId. - Ownership:
buyerTenantId,buyerUserId. - State:
created → pending_payment → paid → fulfilled → refunded | failed. - Invariants:
lines[]non-empty; each line resolves to an active PricingPlan at placement time.totalsequals sum of lines after tax and discount; money-in-micro-units only (no floats).- Idempotency by
clientMutationId.
4.6 License (Marketplace)
- Identity:
LicenseId. - Ownership:
tenantId,listingId,courseId. - Invariants:
remainingSeats ≤ seats.validUntil ≥ validFrom.- State transitions
active → expired | revoked; no return toactive.
4.7 Assignment (Assignment)
- Identity:
AssignmentId. - Ownership:
tenantId,createdBy. - State:
draft → active → paused → archived(paused can return to active). - Invariants:
rruleis a valid RFC 5545 string OR null (one-shot).dueOffsetandgracePeriodare positive ISO durations.- AI-suggested assignments require human approval before
active.
4.8 ComplianceWindow (Assignment)
- Identity: composite
(assignmentId, userId, occurrenceStart)or surrogate ULID. - State:
open → in_progress → completed | overdue | closed_missed. - Invariants:
- At most one
open | in_progresswindow per(assignmentId, userId)at any time (enforced by unique partial index). graceUntil ≥ dueAt ≥ occurrenceStart.
- At most one
4.9 Enrollment (Enrollment)
- Identity:
EnrollmentId. - Ownership:
tenantId,userId,courseId,courseVersionId. - State:
active → completed | expired | revoked(terminal). - Invariants:
- Unique on
(userId, courseVersionId, source.ref). - Transition
completed → activeforbidden. source.kind ∈ {assignment, purchase, manual, self_signup}.
- Unique on
4.10 PlaySession (Delivery)
- Identity:
PlaySessionId. - Ownership:
tenantId,userId,enrollmentId,deviceId. - State:
init → active → paused → completed | abandoned. - Invariants:
- At most one
activesession per(userId, courseVersionId, deviceId)at a time. - Cursor references blocks that exist in the referenced PlayPackage.
- Offline-mounted sessions require valid
OfflineMount(signature + hash verified).
- At most one
4.11 Attempt (Progress / LRS)
- Identity:
AttemptId. - Ownership:
tenantId,userId,enrollmentId,courseVersionId. - State:
open → closed. - Invariants:
- Append-only Statements;
open → openpermitted;closed → openforbidden. - Statements idempotent on
(statementId)and content hash. - Terminal verb (
completed | failed | abandoned) triggersclose.
- Append-only Statements;
4.12 Certificate (Certification)
- Identity:
CertificateId. - Ownership:
tenantId,userId,courseVersionId,enrollmentId. - State:
pending_offline_verification → issued | revoked(terminal). - Invariants:
proof(JWS) verifies against tenant signing key.evidence.completionRecordIdreferences a valid CompletionRecord.- Provisional certificates must carry
OfflineIssuanceClaim.signedByDevice.
4.13 Prompt (AI Services)
- Identity:
PromptId. - Ownership:
tenantId | null(null = system prompt). - State per version:
draft → active → deprecated. - Invariants:
- A Prompt has ≥ 1 version; only one
activeversion at a time per tenant. - Version bump requires passing eval suite before
active. - Template variables ⊆
inputSchemaproperties.
- A Prompt has ≥ 1 version; only one
4.14 AICompletion (AI Services)
- Identity:
CompletionId. - Ownership:
tenantId,userId,promptId?,modelId. - Invariants:
- Immutable once written.
- Must carry
SafetyVerdictfor both input and output. decisionIdwritten when HITL acceptance occurred downstream.
4.15 LocalMutation (Offline Sync — client + server-replica)
- Identity:
clientMutationId. - Invariants:
- Idempotent by
clientMutationId— repeat submission is a no-op. vectorClock[deviceId]is strictly monotonic per entity per device.baseVersion(if present) must match some real server version or benull(create).- State transitions:
queued → inflight → applied | conflicted | rejected(terminal).
- Idempotent by
4.16 SyncCursor (Offline Sync)
- Identity:
(tenantId, userId, deviceId, scope). - Invariants:
lamportis strictly monotonic per(tenant, scope).- Client advances cursor only after server-confirmed pull.
5. Value Objects Allowed to Cross Context Boundaries
Only the following value objects may appear across bounded-context lines. Anything else must be translated through an ACL.
| Value Object | Shape | Where used |
|---|---|---|
TenantId | branded string | Every aggregate everywhere |
UserId | branded string | Every context referencing a user |
DeviceId | branded string | Identity → Content-Packaging, Delivery, Sync |
CourseId | branded string | Authoring ↔ Catalog ↔ Marketplace ↔ Enrollment |
CourseVersionId | branded string | Catalog ↔ Content-Packaging ↔ Delivery ↔ Enrollment ↔ Progress ↔ Certification |
PlayPackageId | branded string | Content-Packaging ↔ Delivery |
BundleId | branded string | Content-Packaging ↔ Delivery ↔ Sync |
EnrollmentId | branded string | Enrollment ↔ Delivery ↔ Progress ↔ Certification ↔ Marketplace |
ListingId | branded string | Marketplace ↔ Catalog (read-only) |
OrgUnitId | branded string | Tenant ↔ Assignment ↔ Analytics |
AssignmentId | branded string | Assignment ↔ Enrollment |
MediaAssetId | branded string | Media ↔ Authoring ↔ Content-Packaging |
Locale (BCP 47) | string | Everywhere |
ISODate | string | Everywhere |
Money { amountMicro, currency } | record | Marketplace ↔ Billing ↔ Analytics |
AIProvenance | record (see §6) | AI Services → every context that persists AI artifacts |
EventEnvelope headers | see 04 EDA | Every NATS event |
LicenseEnvelope | record (see 03 content-service) | Content-Packaging ↔ Delivery ↔ Sync |
VectorClock { [deviceId]: n } | record | Sync ↔ every replicable context |
Entities (aggregate roots or members) are never shared across contexts. If another context needs data about an entity, it consumes events and projects a read model in its own schema.
6. AIProvenance — Mandatory Shared Value Object
Every aggregate that can hold AI-generated content must define a aiProvenance?: AIProvenance field. The domain layer refuses persistence of any AI-marked artifact without one.
interface AIProvenance {
model: string; // e.g., 'chat-large-v4'
version?: string;
promptId?: string;
promptVersion?: SemVer; // pinned at call time
traceId: string; // W3C traceparent
decisionId?: string; // ties to HITL acceptance record
local: boolean; // true if produced by local-inference
generatedAt: ISODate;
reviewedBy?: UserId;
reviewedAt?: ISODate;
cost?: { microUSD: number; tokens: { in: number; out: number } };
safety: { input: SafetyVerdict; output: SafetyVerdict };
cacheHit: boolean;
}
Downstream consumers may filter, display, or audit any artifact using only this VO — there is no need to join into AI Services tables.
7. Conflict-Resolution Matrix (Offline Sync)
Every replicable aggregate declares exactly one conflict policy. The matrix below is canonical; any new aggregate must be added here.
| Aggregate / Entity | Policy | Rationale | Resolver |
|---|---|---|---|
| Statement (xAPI) | append_only | Statements are immutable facts with their own timestamp | server merges; duplicates de-duped by (actor, verb, object, timestamp, statementId) |
| Attempt | append_only for child Statements; server_authoritative for state | Close is server-driven by terminal verb | server |
| AssistantTurn (Delivery) | append_only | Immutable conversation log | server |
| Block (Authoring, live collab online) | crdt_yjs | Live collaboration uses Yjs | Yjs merge in sync-service |
| Block (Authoring, offline edit) | lww + diff UI on non-CRDT fields; CRDT merge for text | CRDT can handle co-existing offline edits to same text; non-text fields surface conflict | user picks in conflict UI |
| CourseDraft metadata | lww | Title/description rarely conflict | server LWW by timestamp |
| PlaySession cursor | max-of | Furthest progress wins | server computes max cursor by module/lesson/block index |
| Enrollment | server_authoritative | Enrollment is a server-issued fact | client mutation rejected with current server state |
| Assignment | server_authoritative | Admin-only aggregate | rejected |
| ComplianceWindow | server_authoritative | Server materializes RRULE | rejected |
| License | server_authoritative | License grant is server-issued | rejected |
| Order | server_authoritative | Payment saga is server-driven | rejected |
| MediaAsset (metadata) | lww for ad-hoc edits; server_authoritative for status | ready/quarantined is server-decided | server |
| Notification preferences | lww by updatedAt | User may edit from multiple devices | last wins |
| Certificate | server_authoritative post-verification | Provisional claims allowed (append-only), final cert server-issued | server |
| OfflineIssuanceClaim | append_only | Immutable claim | server verifies + decides |
| SearchableDocument | projection | Read-model only; rebuilt from events | N/A |
| Taxonomy | server_authoritative | Admin-only | rejected |
| FeatureFlagOverride | server_authoritative | Admin-only | rejected |
All conflict records are retained 90 days in sync.conflicts for audit regardless of resolution.
8. Anti-Corruption Layers (ACLs)
Each ACL owner below holds the responsibility to keep external chaos out of the domain.
| ACL Owner | External Input | Normalized Internal Shape |
|---|---|---|
| identity-service | OIDC IdToken, SAML Assertion, WebAuthn credential; upstream brokers (Keycloak, Firebase, Okta, Cognito) via OIDC discovery | User, Session, IdentityClaim, Credential |
| ai-gateway-service | Vendor LLM/embedding/TTS/image responses | AICompletion, Embedding, AIArtifact, SafetyVerdict |
| billing-service | Payment processor webhook payload, tax vendor response | Payment, Invoice, Payout, TaxLine |
| content-service | 3rd-party SCORM 1.2 zip | PlayPackage, PackageManifest, AssetReference |
| notification-service | Email/SMS/push provider webhook (bounce/complaint) | DeliveryAttempt, BounceRecord, SuppressionEntry |
| media-service | Uploaded binary + MIME, external capture streams | MediaAsset, AssetVariant, CaptionTrack |
ACL contract: the internal shape is strictly typed; no raw vendor types ever escape the ACL adapter into the application or domain layers. ACL adapters live in infrastructure/adapters/ per service, never in domain/ or application/.
9. Integration Mechanism — Events Only
The only approved cross-context integration mechanism is a domain or integration event on NATS JetStream (subject pattern {service}.{aggregate}.{event}.vN). All other mechanisms are explicitly forbidden:
- ❌ Direct cross-service DB reads.
- ❌ Shared database schemas.
- ❌ Synchronous chains > 2 hops (enforced at API gateway + observability).
- ❌ Cross-context ORM joins.
Exceptions (narrowly allowed): low-latency request/reply via NATS for tenant.resolve_for_request (identity-related context resolution at request-entry) and ai-gateway-service for per-call routing. Both are capped at 1 hop and heavily cached.
10. Aggregates Boundary Rule (What Lives Where)
| Concern | Lives in | Does NOT live in |
|---|---|---|
| Credentials + authentication | Identity | Tenant, application services |
| User profile, role, org membership | Tenant | Identity |
| Course definition (mutable) | Authoring | Catalog |
| Course definition (published, immutable) | Catalog (metadata) + Content-Packaging (artifact) | Authoring |
| Quiz scoring rules | Assessment | Authoring |
| Quiz scoring execution | Assessment | Delivery |
| Statement persistence | Progress | Delivery |
| Recurrence rule evaluation | Assignment | Notification |
| Offline bundle signing | Content-Packaging | Delivery |
| Offline bundle decryption | Player runtime (client) | Server |
| Offline mutation queue | Sync (client + server) | Each domain reinventing it |
| AI prompt registry | AI Services | Consumer services |
| AI safety enforcement | AI Services | Each consumer |
| Tenant isolation enforcement | Every layer | (cross-cutting, not localized) |
11. Clean Architecture Enforcement Per Context
Every service enforces the same layered discipline. The rules below are non-negotiable and verifiable by dependency-graph analysis in CI.
11.1 Domain Layer (/domain)
- Pure TypeScript. No
importfrom NestJS, TypeORM, Prisma, NATS client, Next.js, axios, fetch, Redis, pgvector, AWS SDK, Zod. - Allowed:
crypto(browser + node), date-fns-pure (no i/o), pure data transformations. - Forbidden: any I/O, any framework decorator, any dynamic import, any side-effectful constructor.
- Tested with: Vitest, no mocks beyond the domain itself.
11.2 Application Layer (/application)
- Owns: use-cases, command handlers, DTO assembly, mapping, orchestration of ports.
- Depends on: domain only (compile-time).
- Exposes
ports/interfaces:AIClient,EventPublisher,EmailSender,IdentityResolver,SyncClient,Clock,Random,IdGenerator, etc. - Forbidden: importing infrastructure, using
process.env, callingfetchdirectly.
11.3 Infrastructure Layer (/infrastructure)
- Owns: all adapters (Postgres repositories, NATS pub/sub, HTTP clients, S3 clients), DI wiring, environment config.
- Depends on: application (for ports) and domain (for types only).
- Rule: each adapter must have a corresponding port in application; anonymous infra is not allowed.
11.4 Presentation Layer (/presentation)
- Owns: controllers, GraphQL resolvers (optional), WebSocket handlers, SSE handlers.
- Depends on: application.
- Forbidden: importing domain entities directly in responses — always via DTOs.
11.5 Frontend Mirror
Next.js apps follow the same rule set with app/ + components/ (presentation) → hooks/ + services/ (application) → lib/domain/ (pure TS) → lib/adapters/ (infra). lib/domain/ has zero React imports.
12. Multi-Tenant Discipline Per Aggregate
Every aggregate root has tenantId: TenantId. The domain-layer invariants for tenancy:
- Every cross-reference between aggregates must share the same
TenantId(checked in constructors). - Every repository method requires
tenantIdas a parameter (not implicit). - Every event payload includes
tenantId(envelope + payload redundancy for defense). - Every outbox write validates
tenantIdconsistency before commit.
Postgres RLS + the DB-layer checks in 13 Security & Tenancy §4 are the second line of defense.
13. Why This Design (DDD + Clean Architecture + AI/Offline)
- DDD: Bounded contexts aligned to team ownership + ubiquitous language. Aggregates protect invariants at the domain layer; cross-context relationships are explicit (SK, CS, CF, ACL, OHS, PL). No ambiguous couplings.
- Clean Architecture: Domain has zero framework ties. Infrastructure is replaceable — Postgres → another RDBMS, NATS → another broker — without touching use-cases.
- Event-driven: Enables decoupling, replay, audit, and offline sync over the same event log.
- AI-first: A single
AIProvenanceVO and a singleAIClientport make AI governance consistent across 19 services. Provenance is a domain-layer invariant, not a logging concern. - Offline-first: A single
LocalStoreport, a single sync protocol, and a canonical conflict-resolution matrix mean every context behaves predictably when the network drops.
If this document ever disagrees with another document in the set, this document is authoritative for strategic DDD; the other document must be updated.