04 — Bounded Contexts & Context Map
Covers task 6. Bounded contexts (DDD) are the conceptual decomposition of the domain. They are not services yet — they are the seams along which the modular monolith is organized and, later, where services are extracted (05, 09).
The single most important modeling decision is in §1: the namespace (metadata) and the bytes (storage) are different contexts. Conflating them is the root cause of the dual-write bug (R2) and of leaky storage abstractions (R3).
1. The Core Modeling Insight: Metadata ≠ Bytes
A “file” is two distinct things:
- A node in the namespace — a name, a path, a parent, permissions, versions, tags, an owner. This is truth, it is transactional, it lives in Postgres, and it is owned by the File & Metadata context.
- A blob of bytes — content addressed by hash, living in an object store under some provider. This is owned by the Storage context.
A version points at a blob by content hash. Many versions/nodes (within a tenant) can reference one blob (dedup). The namespace can be reorganized (move/rename/copy) without touching a single byte. This separation is what makes moves cheap, dedup possible, GC safe, and the storage provider swappable.
2. The Contexts
Core domain (where we must be excellent)
| Context | Responsibility | Ubiquitous language | Why it’s core |
|---|---|---|---|
| File & Metadata | The per-tenant namespace tree; nodes, versions, tags, metadata; move/copy/rename; trash. Source of truth. | node, folder, version, path, tag, trash | The product’s spine; everything references it |
| Synchronization | Change journal, device cursors, delta computation, conflict detection & resolution | change, journal, cursor, delta, conflict, conflicted-copy | The headline competency (G2) |
| Storage | Object-storage abstraction; blobs, buckets, providers; presigned URLs; multipart; lifecycle; ref-counted GC | blob, content-hash, object key, provider, presign, refcount | The “pluggable cloud” bet (G4) |
Supporting domain (necessary, not differentiating)
| Context | Responsibility | Ubiquitous language |
|---|---|---|
| Identity & Access | Tenants, users, sessions, API tokens, OIDC, RBAC | tenant, user, role, grant, token, principal |
| Sharing & Permissions | ACLs, internal grants, external share links, inheritance | share, link, grant, permission, expiry |
| Search & Indexing | Derived search indexes over names/metadata/(content) | index, document, query, facet, reindex |
| Notification & Events | Webhooks, email/in-app notifications, fan-out | event, webhook, subscription, notification |
Generic domain (commodity; buy/borrow, don’t over-invest)
| Context | Responsibility | Ubiquitous language |
|---|---|---|
| Billing & Metering | Usage metering, quotas, plan limits (NOT payment processing) | usage, quota, plan, meter, limit |
| Administration & Platform | Tenant lifecycle, feature flags, config, audit views | tenant-admin, flag, config, audit |
Audit is modeled as a cross-cutting concern fed by the event backbone, not a standalone context — every context emits audit-relevant events; one append-only store consumes them.
3. Context Map
How contexts relate. Patterns: Upstream/Downstream, CS Customer-Supplier, ACL Anti-Corruption Layer, CF Conformist, OHS/PL Open-Host Service / Published Language (events).
flowchart TB
classDef core fill:#fde68a,stroke:#b45309,color:#1f2937;
classDef support fill:#bfdbfe,stroke:#1d4ed8,color:#1f2937;
classDef generic fill:#e5e7eb,stroke:#6b7280,color:#1f2937;
classDef infra fill:#d1fae5,stroke:#047857,color:#1f2937;
IAM["Identity & Access<br/>(supporting)"]:::support
FILE["File & Metadata<br/>(CORE — source of truth)"]:::core
SYNC["Synchronization<br/>(CORE)"]:::core
STORE["Storage<br/>(CORE)"]:::core
SHARE["Sharing & Permissions<br/>(supporting)"]:::support
SEARCH["Search & Indexing<br/>(supporting)"]:::support
NOTIF["Notification & Events<br/>(supporting)"]:::support
BILL["Billing & Metering<br/>(generic)"]:::generic
ADMIN["Administration & Platform<br/>(generic)"]:::generic
BUS{{"Event Backbone<br/>(Published Language / OHS)"}}:::infra
IAM -- "U/CS: identity context for every op" --> FILE
IAM -- "U/CS" --> SHARE
IAM -- "U/CS" --> ADMIN
FILE -- "U/CS: commits reference blobs by hash" --> STORE
FILE -- "U: emits NodeChanged" --> BUS
STORE -- "U: emits BlobCommitted/Orphaned" --> BUS
BUS -- "D/CF: consumes NodeChanged" --> SYNC
BUS -- "D/ACL: consumes events → docs" --> SEARCH
BUS -- "D/CF: consumes events → fan-out" --> NOTIF
BUS -- "D: consumes usage events" --> BILL
BUS -- "D: consumes all → audit log" --> ADMIN
SHARE -- "U: authorizes access to" --> FILE
SYNC -- "reads namespace + requests presign" --> FILE
SYNC -- "delta transfer via presign" --> STORE
BILL -- "quota check (sync)" --> FILE
Reading the map
- Identity is upstream of nearly everything — every operation carries a tenant
- principal context. Downstream contexts treat identity as a supplier.
- File & Metadata is upstream of Storage in the commit direction: the namespace decides what is real; storage holds bytes referenced by hash. This asymmetry is deliberate — it is how we beat the dual-write problem (R2).
- The Event Backbone is a Published Language / Open-Host Service. Core contexts publish domain events (via the transactional outbox); supporting/generic contexts subscribe. This decouples the core from its consumers and is what lets Search, Notifications, Billing, and Audit be added/removed/scaled independently.
- Search uses an Anti-Corruption Layer — it translates domain events into its own index documents and never exposes OpenSearch’s model upstream. The index is disposable (R7).
- Sharing is upstream of File access at read time (it answers “may this principal see this node?”) but downstream of File for the namespace itself.
4. Why these seams (and not others)
- Storage is split from File because they have different rates of change (storage adapters churn with providers; the namespace model is stable), different scaling profiles (bytes vs metadata), and different failure semantics.
- Sync is split from File because the change-journal/conflict logic is a distinct competency with its own consistency model (causal), and because it is the most likely candidate for independent scaling and the clearest portfolio story. It reads the namespace but owns the journal.
- Search / Notification / Billing are split because they are derived, eventually-consistent consumers. Keeping them behind the event bus means the strongly-consistent core never blocks on them and they can fail independently.
- Identity is split because it is the security kernel; it must be small, auditable, and reusable by every other context.
5. Context → Aggregate sketch (detail lives in 08-data-model)
- File & Metadata:
Node(aggregate root) →Version→ tags/metadata;Trash. - Storage:
Blob(content-hash root, refcount) → provider/bucket/key;MultipartUpload. - Sync:
Change(journal entry),DeviceCursor,ConflictRecord. - Identity:
Tenant,User,Membership/Role,ApiToken,Session. -
Sharing: Share(grantlink), Permission. - Search:
IndexDocument(derived; not authoritative). - Notification:
Subscription,WebhookEndpoint,Notification. - Billing:
UsageMeter,Quota,Plan.
Each aggregate is a transactional consistency boundary. Cross-aggregate / cross-context consistency is achieved via events, never via distributed transactions (ADR-0006).