01 — Domain: ASC 606 first principles
Status: draft
Date: 2026-07-29
Canonical home: this file in revrec-next
Stance: Design from the accounting problem and the product promise — not from contracts.py, Hotglue joins, or OrderDetails sheet archaeology.
Companion product page: chargebee.com/revenue-recognition-software
Visual: ../visuals/revrec-asc606-first-principles.html
Extract plane: ~/work/temporal_poc (see 05-extract.md)
ADRs: 0001, 0002
0. Direct answer to your challenge
Are we assuming we need all this random merge / asof logic?
No. That was a bad assumption.
merge_asof, subscription-closure reloads, inventing future invoice-linked order lines from ramps, and pandas snapshot joins are not ASC 606. They are a reconstruction strategy: “we only trust Chargebee billing events, so we reverse-engineer a contract/PO view by joining invoices to contract terms on dates.”
ASC 606 needs a contract + performance obligations + recognition schedule.
Billing needs an AR / cash / invoice view.
Those are related but not the same object. Conflating them is why the ETL looks insane.
A greenfield system should model ASC 606 directly. Use Chargebee subscription / contract-term / catalog / amendment signals as contract inputs where possible. Treat invoices / CNs / payments as billing & consideration events — not as the thing that invents the contract via asof joins.
1. The problem RevRec is solving (from the product)
From Chargebee’s RevRec positioning:
| Customer pain | Product claim |
|---|---|
| Monetization changes (usage, credits, ramps, custom terms) create recognition obligations Finance can’t keep up with | One rules engine across recurring / variable / hybrid |
| Manual recon across contracts, invoices, amendments | Automated schedules as billing events occur |
| GL overloaded with detail | Revenue subledger — detail in RevRec, summarized journals to GL |
| Slow close | ASC 606 / IFRS 15 five-step automation + recon + posting |
| Need ARR/NRR/deferred waterfall + drill to contract/line | Reporting on the subledger |
Product shape (what Finance buys):
- ASC 606/IFRS 15 engine (five steps + modifications)
- Revenue subledger (schedules, deferred, unbilled, audit trail)
- AR view (invoices, payments, CNs, aging) — overlaps Accounting Premium
- Close + journal posting to GL
- Reporting (waterfall, SaaS metrics, drill-down)
Integrations (Chargebee Billing, Stripe, NS, …) are how data enters — not the definition of recognition.
2. ASC 606 in one page (the real domain)
Core principle: Recognize revenue to depict transfer of promised goods/services in an amount reflecting consideration the entity expects to be entitled to.
Five steps (the only mandatory “pipeline”)
| Step | Meaning | Typical SaaS/subscription inputs |
|---|---|---|
| 1. Contract | Enforceable agreement; rights; payment terms; commercial substance; collectibility probable | Customer + subscription / order / MSA+SOW; CB subscription + contract_term |
| 2. Performance obligations | Distinct promises (or a series of distinct services with same pattern) | Plan/item access over term; setup; training; hardware; credits/material rights |
| 3. Transaction price | Expected consideration (fixed + estimated variable, constrained) | List/sale amounts, discounts, expected usage, breakage, credits |
| 4. Allocate | Relative standalone selling prices (SSP) | SSP library / observable prices / residual |
| 5. Recognize | When (or as) control transfers — over time or point in time | Ratable over service period; PIT on delivery; proportional on units delivered |
What the standard cares about that spreadsheets usually botch
- Recognition timing ≠ billing timing (deferred revenue / contract liability vs contract asset / unbilled)
- Modifications — prospective vs cumulative catch-up
- Variable consideration + constraint (don’t recognize what will reverse)
- Multi-element allocation when multiple POs share a discount
- Series guidance — daily/monthly SaaS often one PO satisfied over time
What ASC 606 does not require
- Joining every invoice line to a contract_term with
merge_asofon dates - Rebuilding full customer history in a DataFrame every sync
- Inventing “Order Number” as a synthetic key from invoice+sub heuristics
- Making the invoice the primary creator of the performance obligation
Those are implementation accidents of “billing-feed → fake sales order.”
3. How today’s RevRec product models it (clean mental model)
Chargebee’s own docs describe three transaction feeds into the recognition engine:
| Feed | Role in ASC 606 terms |
|---|---|
| OrderDetails | Sales-order / obligation lines — what was sold, service window, amounts to allocate/recognize |
| BillingSchedule | What was invoiced against those orders — billing / AR side |
| ProfessionalServicesDelivery | Evidence of satisfaction for proportional / delivery-based POs |
Plus config: Products, SSP library, Revenue rules (Ratable / Point-in-time / Proportional Performance).
So even in the current product, the intended architecture is:
Contract / order lines → allocate (SSP) → recognition schedule
↑ ↑
(sold promises) (policy)
Invoices / payments → billing / AR / cash application
↑
(consideration timing)
Hotglue Chargebee path tries to manufacture OrderDetails from invoices+subs so the engine has something to chew. That manufacturing step is where contracts.py lives — and where complexity exploded.
Invoice-as-contract / disable_contract_creation modes already admit a simpler world: billing lines ≈ order lines. That exists because many merchants don’t need full contract reconstruction.
4. Separation that should drive greenfield design
┌──────────────────────────────────────────────────────────┐
│ CONTRACT & RECOGNITION SUBLEDGER (ASC 606) │
│ Contract → POs → TP → Allocation → Recognition schedule │
│ Modifications, SSP, deferred / unbilled, waterfall │
└──────────────────────────────────────────────────────────┘
▲
│ events (create/amend/cancel PO;
│ bill; pay; credit; deliver)
┌──────────────────────────────────────────────────────────┐
│ BILLING & CASH (AR) │
│ Invoice, CN, payment, refund, dunning │
│ (Accounting Premium / AC_* lives mostly here) │
└──────────────────────────────────────────────────────────┘
▲
│
┌──────────────────────────────────────────────────────────┐
│ SOURCE FACTS (Chargebee canonical) │
│ Customer, subscription, contract_term, catalog, │
│ invoice, CN, transaction, usage, … │
└──────────────────────────────────────────────────────────┘
Rule: Source facts update either the contract/recognition model or the billing/AR model (sometimes both, with explicit links) — never “one mega join that emits fake orders.”
5. When is recognition actually hard? (essential complexity)
| Scenario | Why ASC 606 needs real logic | Still need asof-join ETL? |
|---|---|---|
| Single SaaS PO, ratable over known term, invoice aligns to term | Almost mechanical | No |
| Multi-PO bundle + discount (sub + implementation) | SSP allocation | No — need POs + SSP, not invoice asof |
| Mid-term upgrade / downgrade | Modification accounting | No — need amendment event + policy |
| Ramp / multi-year price schedule | TP over periods; maybe series | No — ramp is contract data, not invoice archaeology |
| Usage / variable consideration | Estimate + constrain; true-up | No — usage events + policy |
| Credit note | Depends: variable consideration vs modification vs return | Policy table — not reason-code → merge_asof spaghetti as the architecture |
| Free PO / material right | Identify unstated PO; allocate | Config + SSP — not billing joins |
| Proportional PS delivery | Delivery evidence | Delivery feed — already a first-class RevRec input |
Essential complexity = policy + state machine on contracts/POs.
Accidental complexity = reconstructing POs from invoice time-series because the contract wasn’t modeled.
6. What a greenfield recognition engine looks like (no current constraints)
Entities (illustrative)
Contract(customer, currency, start, status, source refs)PerformanceObligation(product/SKU, SSP, method, service_start/end or delivery metric)Allocation(amount per PO from TP)RecognitionSchedule/RecognitionEntry(period, amount, status)BillingDocument/BillingLine(invoice/CN) linked optionally to POCashApplication(payment ↔ invoice)Modification(type, effective date, prospective | cumulative)
Operations (the real “API” of the domain)
open_contract(...)— Steps 1–4amend_contract(...)— modification policyrecord_billing(...)— Steps 3 updates / AR / liabilityrecord_delivery(...)— for proportional / PITrecognize(period)— Step 5 close waterfallspost_journals(period)— subledger → GL
Source mapping (Chargebee-shaped, still first principles)
| Chargebee fact | Likely effect |
|---|---|
| Subscription created + items + contract_term | open_contract / POs from catalog rules |
| Subscription changed (plan, quantity, term) | amend_contract |
| Ramp created/updated | Amend future PO amounts/periods |
| Invoice created | record_billing (not “create order via asof”) |
| Credit note | Policy: amend TP / reverse recognition / AR credit |
| Cancellation | Amend / terminate remaining PO; catch-up per policy |
| Usage | Variable consideration true-up |
Invoices do not invent the contract. They move cash timing and often confirm collectibility / constraint.
Ops store vs ClickHouse
- Transactional ops store: contracts, POs, allocations, schedule rows, billing links, period locks — needs consistent read/write, amendments, idempotent event apply.
- ClickHouse (or similar): waterfalls, ARR metrics, drill-down analytics, recon scans — projections of the subledger.
We keep an ops store because ASC 606 state is mutable and event-sourced, not because Hotglue used Redis. If you later prove a pure event-log + rebuild model, fine — still not “CH as merge-asof SoR.”
7. Implications for “boil the ocean”
| Keep questioning | Likely keep |
|---|---|
| OrderDetails as the truth format | ASC 606 five-step domain model |
| Invoice→order asof reconstruction | Explicit contract/PO from subscription/terms/CPQ |
| Pandas snapshot closure as architecture | Event-sourced apply of source facts |
| Camunda/Hotglue | Temporal extract + domain workflows |
| Assuming 234 ETL fixtures = product truth | Golden accounting scenarios (modification types, multi-PO, usage) |
Parity with today’s Hotglue output is a migration/compatibility goal, not the definition of correctness. Correctness is ASC 606 + audit trail + close.
8. Minimal viable recognition (prove the thesis)
Build the smallest engine that:
- Opens a contract from a CB subscription + one PO (plan) + term dates
- Allocates 100% TP (sale price)
- Recognizes ratably by day/month
- Applies an invoice as billing (deferred ↓ as recognized; AR as billed)
- Handles one upgrade (prospective) and one CN policy
If that covers a large % of Chargebee→RevRec merchants, the mega-join ETL was mostly accidental.
Complex SSP / multi-PO / usage then layer as explicit features — not as emergent pandas.
9. Open product questions (now grounded in ASC 606)
- For Chargebee-native merchants, is the subscription (+ contract_term) the Step-1 contract, or do we still need a synthetic “sales order” abstraction?
- What % of volume is single-PO ratable SaaS vs multi-element / usage / PS? (drives how much engine you build first)
- Are ramps / IBM first-class contract attributes in Billing today, or only recoverable via inference?
- Should Accounting Premium (AR subledger) and RevRec (revenue subledger) share one ledger core with two projections, or stay separate products with a shared extract?
10. Bottom line
- RevRec’s job is ASC 606 revenue subledger + close + reporting, with billing/AR as a sibling concern.
- You do not need merge_asof architecture to be ASC 606-correct.
- You need contracts, POs, allocation, recognition schedules, modification policy, fed by clean source events.
- Today’s Hotglue complexity is largely contract reconstruction from billing — question it hard; replace with an explicit model.
- Transactional ops store = mutable ASC 606 state. ClickHouse = analytics. Temporal POC = extract plane (
~/work/temporal_poc).
Next: fill 03-event-catalog.md and 04-engine-scenarios.md. Architecture: 02-architecture.md.