RevRec brainstorm · Research · Architecture deep (early)

09 — Architecture deep dive (simple system)

Status: draft
Thesis: The product is a small event-sourced apply loop + period close. Complexity elsewhere is optional.

Companion: 02-architecture.md · 08-mvp-spec.md · extract 05-extract.md


1. One diagram

                    ┌─────────────────────────────────────────┐
  Chargebee  ───►   │ EXTRACT (Temporal)                      │
  (API)             │  SourceAdapter · schedules · cursors    │
                    │  → raw lake (immutable jsonl/objects)   │
                    └──────────────────┬──────────────────────┘
                                       │ run manifest
                    ┌──────────────────▼──────────────────────┐
                    │ NORMALIZE                               │
                    │  CB payload → canonical domain events   │
                    │  (see event catalog)                    │
                    └──────────────────┬──────────────────────┘
                                       │
                    ┌──────────────────▼──────────────────────┐
                    │ APPLY (domain services)                 │
                    │  idempotent open/amend/bill/pay/credit  │
                    │  → OPS DB (contracts, POs, schedules,   │
                    │            billing, AR, applied_events) │
                    └──────────────────┬──────────────────────┘
                                       │
              ┌────────────────────────┼────────────────────────┐
              ▼                        ▼                        ▼
        CLOSE workflows          SERVE API                 ANALYTICS
        recognize/period         UI / reports              ClickHouse
        post_journals            recon                     projections

That’s the whole product. No Camunda file spine; no Redis-as-document-DB required; no org-wide “one sync job owns the world” lock as the scale model.


2. Why this stays simple

Concern Simple choice Avoid
Contract truth Rows in ops DB from open/amend Reconstructing POs from invoice time series every job
Incrementality Cursor + event apply Snapshot CSV merge + subscription closure reload
Scale Parallelize by tenant / contract Single org mutex + giant DataFrames
Recognition Schedule lines + recognize(period) Buried in sheet builders
AR / AC Projector on billing events Parallel forever-different file dialect as design
Orchestration Temporal (extract + close) BPMN for linear ETL
Analytics Project to CH Making CH the mutable SoR

3. Data stores (roles)

Store Holds Properties
Raw lake Verbatim CB payloads by run Immutable, replayable
Ops DB Live ASC 606 + AR state Transactional, strongly consistent apply
Cursor DB Extract bookmarks Small, per stream/tenant
ClickHouse Waterfalls, metrics, drill indexes Append/project; rebuildable from ops/events
Object storage for exports Journal files, partner dumps Optional

We do not need Redis + SingleStore + S3 sheets as three competing truths. If Redis appears later, it’s cache — not SoR.


4. Apply path (the critical path)

for event in normalized_batch:
    if already_applied(tenant, source, event_id): skip
    match event.type:
        subscription_created → open_contract
        subscription_changed → amend_contract
        invoice_generated    → record_billing
        payment_succeeded    → record_payment
        credit_note_created  → record_credit
        ...
    write AppliedEvent
    commit

Rules:

  1. Idempotent — safe replay from lake.
  2. Ordered enough — per contract/subscription sequence (Temporal workflow or per-key queue). Global total order not required.
  3. No join back to “all historical invoices” to understand current PO state — state lives in ops DB.
  4. Policies are data/config (mod.*, cn.*), not hardcoded spreadsheet branches.

5. Close path

Temporal schedule (monthly/daily):
  for tenant in due:
    recognize(period)
    build deferred waterfall snapshot
    optional: post_journals(period)
    optional: lock period

Close is a domain workflow, not a side effect of “sync job finished loading Excel.”


6. Multi-tenancy & scale

Lever Approach
Extract Per-tenant Temporal schedules; KEDA scale-to-zero (POC)
Apply Workers competing on event partitions keyed by tenant_id or subscription_id
Fat tenant Split apply by subscription key; never load full org history into memory
Isolation Fail one tenant without blocking fleet

This directly attacks today’s “skewed fat tenant OOM/timeout” failure mode.


7. RevRec vs Accounting Premium

                domain events
                     │
         ┌───────────┴───────────┐
         ▼                       ▼
   Revenue projector        AR projector
   (contracts, POs,         (invoices, AR,
    schedules, deferred)     cash, CN, aging)
         │                       │
         └───────────┬───────────┘
                     ▼
              shared ops DB
              (or shared event log + two read models)

Same extract. Two projectors (or one ledger with two account spaces).
Do not design AC as a forever-separate Hotglue dialect — that can be a temporary bridge.


8. Mapping from current system (what moves where)

Today Tomorrow
tap-chargebee / Hotglue extract SourceAdapter + Temporal
contracts.py reconstruction Delete as architecture; steal CN/mod edge policies into named policies
OrderDetails / BillingSchedule files Ops DB entities (or ephemeral export if UI needs)
Camunda NEW_SYNC_JOB Extract run + Apply batch (+ separate Close)
Redis SO documents Ops DB
Warehouse recognition recognize + schedule lines
AC_* reshape AR projector
MSTR API + CH

Migration = shadow accounting compare + cohort cutover (02 phases A–F).


9. Suggested build order (engineering)

  1. Ops DB schema + apply API in-process (no CB yet) — drive with S01–S05 fixtures.
  2. Extract Chargebee subset → lake (Temporal POC).
  3. Normalize mapper CB → events.
  4. Wire apply to live pulls for one dogfood tenant.
  5. Close + journal export.
  6. AR projector for AC-shaped needs.
  7. Only then expand policies (multi-PO, usage).

Do not start by porting part_processor.py.


10. Complexity budget

Allowed complexity:

Disallowed as default architecture:

If a feature needs complexity, it gets an ADR + scenario — not a new pandas epic.