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:

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”:

  1. Partition work by (tenant_id, contract_id).
  2. Schedule lines: day grain in SoR; optional period rollup tables for read.
  3. Close: sharded recognize across contract ranges.
  4. When single Postgres instance saturates: shard/partition by tenant or contract hash — same schema, more nodes.

7. Security & tenancy


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

10. Relationship to other docs