RevRec brainstorm · Domain & architecture · Architecture from model
18 — Architecture from the domain model
Status: normative architecture (model-first)
Premise: Derive runtime shape from ASC 606 domain needs — not from whatever an incumbent stack used.
Companions: 14-domain-model.md · 13-full-design.md · ADR 0004
1. Method
Domain invariants & operations (doc 14, ASC 606)
↓
Required system properties (consistency, audit, scale, close)
↓
Component roles (what kind of store/workflow/API)
↓
Technology choices (with alternatives)
If a technology does not serve a domain property, it does not belong in the design.
2. Domain properties → system properties
| Domain need | System property |
|---|---|
| Contract/PO/schedule are authoritative write model | Single transactional system of record for subledger state |
| Every balance change cites source + policy | Append-only audit / applied-event log with idempotency keys |
| Amendments rebuild remaining schedules precisely | Fine-grained schedule storage + versioned allocations |
recognize(period) / lock must be safe under concurrency |
Period state machine + per-contract apply serialization |
| Fat tenants, many contracts | Horizontal parallelism by contract key; no global exclusive lock as scale model |
| Replay after policy fix / disaster | Immutable source snapshot lake + rebuildable projections |
| Waterfalls, RPO, ARR at interactive latency | Read-optimized analytical projections (rebuildable, not SoR) |
| Billing ≠ recognition | Separate billing aggregates linked by ids, not merged invent-PO paths |
| AC journals amounts RevRec calculated | Published recognition facts interface to AC bounded context |
| Multi-source later (CB first) | SourceAdapter → canonical events; core never speaks vendor JSON |
3. Component roles (technology-agnostic)
| Role | Responsibility | Must provide |
|---|---|---|
| SourceAdapter | Authenticate, page, emit raw payloads | Checkpoints, backfill, schema version tags |
| Raw lake | Keep verbatim source payloads | Immutability, retention, replay by tenant/stream/run |
| Normalize | Map raw → canonical domain events | Pure-ish functions, quarantine on map failure |
| Apply services | Idempotent domain commands | ACID with ops SoR; policy registry |
| Ops SoR | Contracts, POs, schedules, billing, periods | Strong consistency, constraints, multi-tenant keys |
| Orchestrator | Long-running extract/apply/close | Resume, visibility, per-key concurrency limits |
| Query/API | Commands + reads + compat projections | Authz, tenant isolation |
| Analytics projector | Waterfalls/metrics | Eventually consistent OK; rebuild job required |
| AC consumer | Journals from published amounts | Dr=Cr; own COA; close after RevRec |
4. Reference runtime (chosen defaults)
Defaults are role fillers. Swap within a role without changing the domain.
Source systems (Chargebee first; adapters later)
│
▼
┌───────────────────┐ ┌────────────────────────────┐
│ Orchestrator │────►│ Raw lake (object store) │
│ (workflow engine)│ └────────────────────────────┘
└─────────┬─────────┘
│ normalize
▼
Canonical domain events
│
▼
┌───────────────────┐ ┌────────────────────────────┐
│ Apply workers │────►│ Ops SoR (relational DB) │
│ (domain services) │ │ + applied_events (audit) │
└─────────┬─────────┘ └─────────────┬──────────────┘
│ │
│ close workflows ├── outbox → Analytics OLAP
▼ └── recognition facts → AC
Period recognize / post / lock
│
▼
API / UI
4.1 Why these defaults (from domain, not fashion)
| Role | Default | Domain reason | Strong alternatives |
|---|---|---|---|
| Ops SoR | PostgreSQL | Relational invariants (FK PO→contract, unique applied event, period states, schedule queries) | Cockroach/Spanner if multi-region active-active becomes a hard req; not required to start |
| Lake | S3/GCS | Cheap immutable blobs for replay | Azure Blob, MinIO |
| Orchestrator | Temporal | Per-contract workflows, close progress, backfill resume match apply semantics | Other workflow engines with same properties |
| Apply language | Python | Policy velocity; typed ports later if needed | JVM/Go workers behind same API |
| Analytics | ClickHouse | Period×contract aggregations, waterfall drill | BigQuery/Snowflake/Pinot if org-standard |
| API | Stateless HTTPS | Clear command/query boundary | — |
4.2 What we do not put in the design as requirements
We do not inherit incumbent components as goals or anti-goals. The design simply does not need:
- A second mutable document store competing with Ops SoR
- Spreadsheet/CSV manufacturing as the definition of a PO
- A global “one job owns the tenant” mutex as the concurrency model
- An OLAP warehouse as the write-side recognition engine
If an existing deployment uses other tools, migration adapters speak canonical events; they are not part of the target domain architecture.
5. Consistency model
| Concern | Rule |
|---|---|
| Idempotency | (tenant_id, source, source_event_id) unique |
| Ordering | Total order per contract; cross-contract parallel |
| Apply TX | State mutation + applied_events (+ outbox) atomic |
| Analytics | Async projection; reconcile totals to Ops SoR |
| Close | No recognize into HARD_LOCKED; PPA in next period |
6. Scale model
Derived from “many contracts per tenant, heavy schedule cardinality”:
- Partition work by
(tenant_id, contract_id). - Schedule lines: day grain in SoR; optional period rollup tables for read.
- Close: sharded recognize across contract ranges.
- When single Postgres instance saturates: shard/partition by tenant or contract hash — same schema, more nodes.
7. Security & tenancy
- Every row carries
tenant_id. - API authz enforces tenant.
- Optional DB RLS as defense in depth.
- Secrets for source adapters in a vault; least privilege.
8. Observability (domain-shaped)
| Signal | Why |
|---|---|
| Extract lag by stream | Freshness for close gate |
| Apply error / quarantine queue | Bad events must not silent-skip |
| Open schedule $ by period | Close readiness |
| Accounting identity violations | Engine correctness |
| Projection drift vs SoR | Analytics trust |
9. Evolution
| Change | Allowed without new domain ADR? |
|---|---|
| Swap CH ↔ BigQuery | Yes (analytics role) |
| Swap Temporal ↔ equivalent orchestrator | Yes if properties preserved |
| Make OLAP the SoR | No — new ADR; breaks invariants |
| Add SourceAdapter | Yes |
| Change Contract/PO meaning | No — domain ADR |