Cost Allocation & Showback Platform

Architecture Views

22 views, in reading order. Every view ships three ways: an HTML page, an SVG that re-opens in diagrams.net fully editable, and draw.io source.

Read the set in order. Act 1 fixes the boundary between the bill and the organisation; act 2 says who is actually served and what they get to do; act 3 gives the structure; act 4 says who wrote which data and what losing it costs; act 5 shows what happens when a period is allocated, restated and disputed; act 6 covers how the platform is run and how a policy change ships; act 7 covers why estate-wide cost data is safe to hold. Each page carries the diagram, the reasoning behind it, and links to the editable source.

1 · Context and scope

The boundary: who reads the number, where the spend is billed, and what this platform deliberately does not do.

2 · People and journeys

Who the platform is for, and the two journeys that decide whether the number is believed.
03 Engineering Engineering lead one of 450 Goal — Tell me what my team costs, and let me prove to my own engineers which change caused the number to move. Core journeys Explain a cost spike the load-bearing journey Fix my team's tag gaps Set a budget for next quarter Shared platform owner clusters, warehouse Goal — Do not hand me the whole cluster bill. Split it by a rule my tenants can read and argue with. Core journeys Publish an apportionment rule Defend the idle line Finance and FinOps Finance controller monthly close Goal — Give me one frozen number per cost centre that reconciles to the invoice and does not change after I have reported it. Core journeys Close and publish the month 5 business days Absorb a restatement FinOps analyst policy owner Goal — Let me change an allocation rule and see, before I adopt it, exactly whose bill it moves and by how much. Core journeys Simulate a policy change Chase the unallocated remainder Product owner 28 lines Goal — Show me cost per order, and tell me whether it went up because we grew or because we got worse. Core journeys Review unit economics Machines and feeds Provider billing export 3 clouds, daily Goal — I deliver yesterday, and I silently rewrite last week. Cope with that. Core journeys Deliver a restated period Business driver feed team-owned Goal — I am a denominator somebody else maintains, and I go stale without telling anyone. Core journeys Post a daily driver value Actors and Their Core Journeys Person or role Journey / task External / third party Security / platform The shared platform owner is in the cast because apportionment is negotiated with them, not imposed on them. v 1.0 · owner Platform Architecture · date 2026-09 Actors and Their Core Journeys Seven actors, including two that are not people and one who is usually forgotten: the owner of the shared cluster. HTML page SVG draw.io

3 · Structure

The layers, the deployed components, and the integration surface.
06 Experience Team dashboard 13 months Statement viewer Drill-through explorer Dispute desk Query & export API Statement (immutable) Statement publisher freeze at close Version register Restatement controller supersede only Dispute ledger GL posting builder Analytics Unit-economics engine Budget & forecast Anomaly detection Policy what-if simulator Allocation (pure function) Run orchestrator Direct attribution Shared-cost apportioner Commitment amortiser Remainder handler Policy registry versioned Ownership Effective-dated org tree Tag & mapping resolver Exception registry Coverage scorer gap in currency Normalisation FOCUS conformer Service & SKU taxonomy FX fixing fixed at close Restatement detector Invoice reconciler Schema quarantine Ingestion Provider export readers Licence & SaaS feeds Container usage collector Business driver intake Raw landing zone as given, forever Consumed (not owned) Provider billing APIs Service catalogue Org & cost-centre system Identity provider Cluster metrics ERP general ledger version pinned allocated facts Layered Architecture — Eight Layers, Three of Them Immutable Application we own Interface / broker Data store Security / platform Risk / gap External / third party synchronous Ownership sits below allocation on purpose: allocation reads a snapshot of it, never the live table. A detected restatement re-enters at the run orchestrator, not at the statement. v 1.0 · owner Platform Architecture · date 2026-09 Layered Architecture Eight layers. Ownership sits below allocation on purpose — allocation reads a snapshot of it, never the live table. HTML page SVG draw.io

4 · Data

What is stored, who wrote it, what losing it costs, and what can be recomputed.
11 billing_period period_id PK provider start_date / end_date state open|closed|restated fx_rate_fixed_at raw_export export_id PK period_id FK provider_version landed_at sha256 cost_record cost_record_id PK export_id FK resource_id FK charge_period_start billed / effective / amortised revision_no resource resource_id PK provider_scope_id service / sku tags jsonb first_seen / last_seen ownership_assignment assignment_id PK resource_id FK team_id FK valid_from / valid_to rule_fired confidence team team_id PK cost_centre_id FK product_line_id FK valid_from / valid_to policy_version policy_version_id PK adopted_at apportionment_rules jsonb remainder_strategy savings_attribution shared_pool pool_id PK policy_version_id FK scope_selector basis usage|request|even|weighted idle_treatment allocation_run run_id PK period_id FK billing_snapshot_id ownership_snapshot_id policy_version_id FK started_at / digest allocated_cost allocated_id PK run_id FK cost_record_id FK team_id FK basis / share amount amortised commitment commitment_id PK purchased_at term / prepaid_amount covered_scope attribution_mode business_driver driver_id PK team_id FK definition_version reported_for_date value / cadence statement statement_id PK team_id FK period_id FK version_no run_id FK frozen_at / supersedes statement_line line_id PK statement_id FK category amount allocated_ref dispute dispute_id PK statement_id FK raised_by / raised_at state resolution correct|reject gl_posting posting_id PK statement_id FK cost_centre_id amount idempotency_key 1 : N 1 : N 1 : N 1 : N 1 : N 1 : N 1 : N 1 : N 1 : N 1 : N 1 : N 1 : N 1 : N 1 : N 1 : 1 Data Model — The Spine Is Period, Cost Record, Ownership, Run, Statement allocation_run carries all three input versions. Any allocated_cost row is therefore reproducible, and any statement is traceable to the exact inputs that made it. v 1.0 · owner Platform Architecture · date 2026-09 Data Model The spine is period → cost record → allocation run → allocated cost → statement. Every run carries all three input versions. HTML page SVG draw.io

5 · Runtime

Allocation, restatement, apportionment, dispute and detection as they actually run.

6 · Operations

Where it runs, how a policy change ships, what is watched, and the loop it lives in.

7 · Assurance

Why holding every team's cost data in one place is safe.

Architecture One-Pager

The problem, the shape of the answer, the decisions that carry it, and what a prototype should prove.

Every published number is a frozen artefact produced by a pure, versioned function of three immutable inputs — never a live query over mutable data.

The provider bills an account, a subscription or a project. The organisation wants to know what a team, a product and a customer cost. Between the two sits an allocation problem that is mostly metadata and politics and only partly arithmetic, and it is made hard by three things at once: cloud billing data is restated for weeks after the usage occurred; a large share of spend is incurred by shared infrastructure that no single team owns; and the organisation whose costs are being attributed is itself moving, as teams split, resources change hands and the apportionment rules are argued over and revised. A platform that answers by querying the current state of all three will give a different answer to the same question on two consecutive days and be unable to say why. That is the single failure that ends a showback programme, because a number nobody can reproduce is a number nobody will accept.

Land every provider export immutably and exactly as given. Normalise beside it, never over it, onto one FOCUS-conformed cost record, and reconcile the total to the provider's own invoice before anything downstream may publish. Keep ownership as an effective-dated model resolved by a declared rule chain, so a snapshot can be taken as of any instant. Keep allocation policy as a versioned, simulatable artefact outside the engine. Then make allocation a pure function of exactly three pinned inputs — billing snapshot, ownership snapshot, policy version — whose output is derived, disposable and rebuildable. Freeze that output into a statement that carries all three versions, supersede it rather than overwrite it when the inputs move, and let nothing downstream of the freeze read anything that can still change.

What it is, and what it is not

A pure function of three pinned, immutable inputsA dashboard that queries live billing and current ownership
A raw zone that is the complete, immutable source of everythingA staging area that is cleared once the warehouse is loaded
An unallocated remainder published as its own lineA proportional spread that balances to 100% and hides the gap
Restatement as the normal path run a second timeRestatement as an exception workflow with its own code
A frozen statement version that supersedes its predecessorA statement table updated in place at month end
A read-only measurement platformA cost-optimisation tool that resizes or deletes resources

The decisions that are the architecture

01Allocation is a pure function of three pinned inputs

Billing snapshot, ownership snapshot and policy version are resolved and hashed before any arithmetic happens, and every allocated row and every statement carries all three. 'Why did my number change?' is answerable in one sentence: which of the three moved.

ADR-01

02The bill is landed as given, forever

Every provider export is written immutably to the raw zone, versioned by provider, period and export version. Normalisation happens beside it. Everything downstream is derived from it, which is what makes any period rebuildable years later.

ADR-02

03Allocated facts are materialised, not resolved at query time

A statement must be an artefact that exists, can be frozen, and can be pointed at during a dispute. Query-time resolution makes policy iteration free and makes publication meaningless.

ADR-03

04Restatement supersedes; it never overwrites

A re-issued export lands as a new version, diffs at record grain, re-runs the same pure function, and produces statement v2. v1 survives for seven years, and so does the reason for the delta.

ADR-04

05Everything about the organisation is effective-dated

Ownership resolves by a declared precedence chain — tag, parent scope, mapping table, exception — as of a point in time, and records which rule fired. A reorganisation changes the future, not the meaning of a closed period.

ADR-05

06The remainder is a named line, not a spread

Unallocated spend is published as its own line in every view at every level, and charged to a named accountable owner rather than distributed proportionally. Balancing to 100% is exactly why tag coverage never improves.

ADR-07

07Cluster idle lands on whoever sized the cluster

Shared compute is apportioned on max(request, usage), and the reserved-but-unused remainder goes to the platform owner — the only party who can act on it — rather than being averaged across tenants who cannot.

ADR-09

08Reconciliation is a gate, not a metric

A period whose ingested total does not match the provider invoice within 0.1% or $500 does not publish. Showback may be late; it may not be quietly short.

ADR-11

09Frozen and current are never the same figure

The current period is served from the same pipeline, explicitly labelled as an estimate, and never rendered in the same number as a closed statement. One path, two labels.

ADR-12

10Access is scoped by accountability, server-side

The scope predicate is resolved from the effective-dated org tree and injected into every query; a parameter cannot widen it. Out-of-scope reads are answered at aggregate grain rather than refused.

ADR-16

Why this should still be right in ten years

Cloud service catalogues, billing formats and organisational shapes will all change inside the life of this platform. These are the properties that should outlast them.

Purity outlives the engine

The contract is that identical inputs produce identical output. Whether the engine is Spark, a warehouse or something that does not exist yet, the contract is unchanged and every past period stays reproducible.

Immutable raw survives every format change

FOCUS will be revised; providers will add charge categories and retire SKUs. Because the conformed layer is derived and the raw layer is not, a new conformer re-derives history rather than losing it.

Effective-dating survives every reorganisation

The one thing guaranteed to happen repeatedly is that the organisation changes shape. A model that stores ownership as of a time rather than as of now does not need to be rebuilt when it does.

Policy as data survives the argument

Apportionment rules are the part of this system people will keep changing, because they encode a negotiation rather than a fact. Keeping them versioned and outside the engine means the argument costs a review, not a release.

A derived, disposable fact store survives growth

The largest store is the one that can be thrown away. As volume grows forty per cent a year, the cost decision about retention stays reversible, because nothing irreplaceable lives there.

Showback and chargeback share one ledger

The move from informational to billing is a governance change, not a re-platforming, because enabling chargeback changes who acts on the number rather than how it is computed.

Non-functional targets

Every figure below is a stated assumption for this design. They are chosen to be defensible and arguable; a reviewer who changes one can follow it to the decision that depends on it.

QualityTargetHow it is metView
Reporting availability ≥ 99.5% monthly; publication and posting ≥ 99.9% around close Zone-redundant serving tier separated from batch, so no interactive request waits on an allocation run 17
Data freshness Conformed by 09:00 T+1 p95; allocated by 10:00 T+1 p95, ceiling T+2 Hourly export polling with a declared cutoff, then a daily incremental allocation run 13
Allocation run time Full month-to-date re-run p95 ≤ 45 min, hard ceiling 90 min Partition-parallel Spark on interruptible capacity, checkpointed per partition 12
Query latency Dashboard p95 ≤ 2 s over 13 months; drill-through p95 ≤ 10 s Pre-aggregated roll-up marts for dashboards, full-grain fact store reserved for drill-through 09
Attribution completeness ≥ 98% of spend to a named owner at close; remainder ≤ 2% Rule-chain resolution plus an owner-facing reconciliation surface ranked by cost 16
Reconciliation accuracy Within 0.1% or $500 of the provider invoice, whichever is larger Per-provider invoice comparison as a publication gate, not a dashboard metric 12
Reproducibility Identical inputs produce bit-identical output Run digest over the three pinned input ids; a matching digest returns the existing result 12
Durability RPO 0 for statements, disputes, ownership and policy; RPO 24 h for conformed facts Synchronous commit and geo-replication for human-authored and published data only 10
Recovery RTO 4 h reporting, 1 h on close days, 24 h for full fact re-derivation Cold serving in the recovery region; the fact store is rebuilt from the replicated raw zone 17
Scale 1.2 bn cost records and 2.6 bn usage samples per month, growing 40% a year Columnar fact store partitioned by period and provider; telemetry downsampled on ingest 07
Detection Sustained ≥ 30% day-over-day increase routed within 24 h; ≤ 5 false positives per team per quarter Detect on allocated facts so the alert already has an owner; suppression windows for declared events 16
Platform cost ≤ 0.5% of the spend it measures Lifecycle-tiered raw storage, spot batch capacity, pre-aggregated serving; published as a line in its own report 17

Scope

In scope

  • Ingestion and FOCUS conformance of billing data from three clouds, plus licence and SaaS spend
  • Effective-dated ownership resolution and tag-coverage measurement in currency
  • Shared-cost apportionment, commitment amortisation and remainder handling under versioned policy
  • Frozen monthly statements with drill-through, restatement and a dispute workflow
  • Budgets, forecasts and anomaly detection routed to the resolved accountable owner
  • Unit economics from team-reported business drivers
  • A GL posting path for chargeback, generated only from frozen statements

Explicitly out of scope

  • Procurement, contract negotiation and the decision to buy a commitment
  • The general ledger itself — the ERP remains the book of record
  • Rate optimisation actions: the platform reports opportunities and never changes a resource
  • Capitalisation, depreciation and headcount cost
  • Tag enforcement at resource creation, which belongs to the cloud governance platform

What a four-week prototype should prove

The prototype's job is to falsify the central claim — that allocation can be a pure, reproducible function over data that is still moving — on one cloud and one month of real spend.

  1. Land one provider's detailed export immutably, then land a restated version of the same period and show the record-grain diff
  2. Resolve ownership by the rule chain against real tags, and publish the coverage gap in currency rather than resource count
  3. Run allocation twice on identical pinned inputs and show bit-identical output and a matching digest
  4. Re-run the same period after a single policy change and produce a per-team delta report
  5. Freeze a statement, then restate it, and retrieve both versions with the delta explained to resource level
  6. Apportion one shared Kubernetes cluster from real usage telemetry and show where the idle went
  • The export for one provider does not arrive: the period publishes as visibly incomplete, naming the provider
  • The export schema gains an unrecognised column: the partition is quarantined and the provider's period is blocked
  • A team is split mid-period: both successors receive the correct share and the closed prior period is unchanged
  • Usage telemetry is lost for six hours: the affected allocations are marked degraded and excluded from chargeback
  • A bad apportionment rule is adopted: it is rolled back in one command and the affected periods re-run inside the batch window

Open risks, carried rather than hidden

RiskIf it landsResponse
Tag coverage never improves, and the remainder stays above the 2% target The number stays arguable, chargeback never becomes credible, and the platform is treated as a report Charge the remainder to a named accountable owner rather than spreading it; publish coverage per team in currency; make the reconciliation surface rank gaps by cost so fixing metadata is cheaper than disputing the bill
Apportionment becomes a permanent political negotiation rather than a rule Policy churns every period, statements restate constantly, and the ledger loses the stability that made it trusted Policy is versioned, simulated against a closed period before adoption, published to tenants before it applies, and reviewed on the close cadence rather than on demand
Provider restatements arrive after the correction window and after the ledger has been reported Finance holds a number the platform can no longer reproduce, and the two records diverge permanently A 90-day correction window with supersede-not-replace versioning; restatements past the window are carried as a labelled adjustment in the current period rather than silently applied
The fact store's growth outruns the 0.5% platform-cost target The platform that measures efficiency becomes a line item someone else has to justify The fact store is derived and disposable: retention can be shortened without data loss because the raw zone is the archive of record; roll-ups absorb dashboard traffic
Chargeback is enabled before the ledger is trusted Every dispute becomes a budget dispute, the workflow saturates, and teams route around the platform with their own spreadsheets Showback earns chargeback: the same ledger, gated on sustained attribution completeness and dispute volume rather than on a date in a programme plan

Architecture Decision Record

Why every component and every technology on these 22 views is what it is, and what each choice costs.

Sixteen decisions make up this architecture. Everything else across the twenty-two views is either a consequence of one of them or a detail that could be decided differently next quarter without anybody noticing. Each record states the question as it was actually open, the options that were weighed, what was chosen, how it is realised on Azure, what the choice costs, and the conditions under which a different answer would be right.

Status of this document. This is a design, not a report on a running system. Every rate, latency, tolerance, threshold and retention figure is a stated assumption chosen to be defensible and arguable rather than measured — the operating context is an estate of three clouds, roughly 1,200 billing scopes, 40,000 active resources, 450 engineering teams, and about $180M a year of cloud spend plus $40M of licence and SaaS spend, which is itself an assumption. Where a number is load-bearing it is named in the record that depends on it, so a reviewer can change the number and see which decisions move with it.

How to read a record

QuestionThe forcing question: why a decision was needed at all.
ContextThe requirement, the scale and the constraint that make it hard.
DecisionWhat this architecture does, stated so it can be checked.
How it is realised on AzureThe concrete mechanism: which service or package, configured how, in which subscription.
Options weighedChosen, rejected, deferred, or right elsewhere, with the reason for each.
ConsequencesWhat the choice buys and what it costs, both kept visible.
Choose differently whenThe conditions that would flip the decision for your system.
Why it holds up over timeWhat keeps the decision right as scale, staff and technology change.
LessonThe principle that transfers beyond this platform.

Decision map

Reproducibility and the ledger 4

The decisions that make a published number defensible while its source data is still moving.

ADR-01Allocation is a pure function of three pinned, versioned inputs ADR-02The bill is landed exactly as given, immutably, and normalised beside it ADR-03Allocated facts are materialised per run, not resolved at query time ADR-04Restatement supersedes, and is the normal path run a second time

Ownership and attribution 4

How a cost record acquires an accountable owner, and what happens to the ones that do not.

ADR-05Ownership is effective-dated and resolved by a declared precedence chain ADR-06A mid-period ownership change is effective-dated, not retroactive ADR-07The unallocated remainder is a named line charged to an accountable owner ADR-08Inferred attribution is marked, and excluded from chargeback

Shared cost and commitments 2

Splitting what nobody incurred alone, and deciding where a discount lands.

ADR-09Shared compute is apportioned on max(request, usage), with idle to the platform owner ADR-10Commitment savings go to the consumer, with both amortised and cash views published

Publication and disputes 3

Freezing, restating, disputing and posting — the lifecycle of a number people act on.

ADR-11Reconciliation to the provider invoice gates publication ADR-12One pipeline, two labels: the current period is an estimate, never a statement ADR-13Disputes never block publication, and close by correction or reasoned rejection

Scale, storage and cost 2

What is kept, for how long, at what grain, and what the platform is allowed to cost.

ADR-14Full grain is kept in the raw zone; allocated facts are kept 13 months and are disposable ADR-15Serving and batch are separate tiers, and batch runs on interruptible capacity

Access and confidentiality 1

Who may see whose bill, and why estate-wide cost data is sensitive.

ADR-16Scope is resolved server-side from the org tree; out-of-scope reads are answered at aggregate grain

Technology by capability

Azure was chosen for this exercise deliberately rather than because the topic demands it: a showback platform is inherently multi-cloud in its inputs and has to run somewhere, and the recent use cases in this practice have leaned on Google Cloud and on open-source on-premises stacks. Every requirement in ask.md is vendor-neutral; the table below is one defensible realisation of it. The load-bearing property of each choice is named, so a reader on another cloud can substitute the equivalent without losing the architecture.

Open source This design
CapabilityChoiceOriginCredible alternativeWhy this oneRecord
Raw landing zone ADLS Gen2 with immutable blob policy and lifecycle tiering Azure S3 with Object Lock, or GCS with bucket retention Write-once semantics enforced by the platform rather than by convention, and tiering to archive without changing the path ADR-02
Cost fact store Azure Data Explorer, partitioned by period and provider Azure Synapse dedicated pools, BigQuery, Snowflake, or Delta on Databricks SQL Columnar scan over a 13-month window with materialised roll-ups, and cheap partition drop for retention ADR-03
Allocation engine Azure Databricks, partition-parallel, on spot node pools Azure Synapse Spark, EMR, Dataproc, or a warehouse-native SQL pipeline Deterministic partitioned execution with checkpointing, which is what makes a pure re-run affordable on interruptible capacity ADR-15
Ingestion orchestration Azure Data Factory with per-provider pipelines Azure Airflow, Step Functions, or Cloud Composer Declarative per-source scheduling with landing manifests and a clear cutoff-and-escalate semantic ADR-11
Ownership, policy and statements Azure SQL Database, zone-redundant, geo-replicated Azure PostgreSQL Flexible Server, Aurora, or Cloud SQL Temporal tables for effective dating, synchronous commit for RPO 0, and transactional publication of a frozen statement ADR-05
Policy registry Git repository as the source, versions pinned in Azure SQL Open source + Azure A policy table with an approval workflow, or OPA bundles Review, history and rollback come free; the pinned version id is what the allocation run actually reads ADR-01
Usage telemetry transport Event Hubs with downsampling on ingest Azure Kafka, Kinesis, or Pub/Sub 2.6 bn samples a month at bounded retention, with a consumer that can fall behind without losing the window ADR-09
Serving tier AKS across availability zones, autoscaled Azure App Service, Cloud Run, or ECS Serving isolated from batch so no interactive request ever queues behind an allocation run ADR-15
Identity Entra ID with workload identity and enforced MFA Azure Okta with OIDC, or any IdP issuing group claims Short-lived machine credentials with no static keys, and group claims that name the person rather than their scope ADR-16
Key management Key Vault with customer-managed keys and rotation Azure KMS, Cloud KMS, or HashiCorp Vault Rotation without re-ingestion, which matters when the raw zone is immutable and 37 months deep ADR-16
Provider billing access Per-provider export-read-only credentials in Key Vault Multi-cloud Any least-privilege billing reader role Credentials that read the bill cannot reach the workloads, which is what makes estate-wide access acceptable ADR-16
Billing schema FOCUS-conformed cost record, provider fields retained FinOps Foundation A bespoke internal schema One vocabulary across three clouds, with the provider-native columns kept so nothing is lost in translation ADR-02
Dashboards and marts Pre-aggregated roll-ups served to Power BI and the platform UI Azure Looker, QuickSight, or Superset Bounded query cost per team, with full-grain reads reserved for drill-through ADR-03
Observability Azure Monitor, alerting on correctness rather than availability Azure Prometheus and Grafana, or any SLO tooling For a measurement platform, being wrong is worse than being late, and the alert policy has to say so ADR-11

The decisions, and the alternatives that lost

Reproducibility and the ledgerThe decisions that make a published number defensible while its source data is still moving.

ADR-01

Allocation is a pure function of three pinned, versioned inputs

Accepted

When someone asks why their cost number changed between yesterday and today, what does the architecture allow us to say?

Context
Three things move independently and continuously. Provider billing data is restated for weeks after the usage occurred, by credits, commitment reallocation and provider corrections. The organisation changes: teams split, resources change hands, cost centres are renamed. And the allocation rules themselves are argued over and revised, because they encode a negotiation rather than a fact. A platform that resolves attribution by reading the current state of all three produces a different answer to the same question on consecutive days, with no mechanism for explaining the difference. Every showback programme that loses credibility loses it here, and a ledger nobody can reproduce can never be promoted to chargeback.
Decision
Allocation is defined as a pure function over exactly three immutable, separately versioned inputs: a billing snapshot, an ownership snapshot taken as of a point in time, and an allocation policy version. The three are resolved and pinned before any arithmetic happens, hashed into a run digest, and recorded on every allocated row and every published statement. A run with a digest that already exists returns the existing result rather than recomputing it. Nothing is allocated in place, and no published figure is ever a live query.
How it is realised on Azure
The run orchestrator resolves the three ids and writes an allocation_run row in Azure SQL before dispatching. Databricks jobs read the pinned billing snapshot from the Azure Data Explorer fact store by version predicate, the ownership snapshot as an as-of query against the effective-dated tables, and the policy document at its pinned version from the registry. The digest is a hash of the three ids plus the engine version; allocated rows carry run_id, and statements carry the three ids directly so they are visible on screen.
Options weighed
  • ChosenPure function of three pinned inputs: Reproducible, explainable, and makes restatement a re-run rather than a special case
  • RejectedResolve attribution against current state on each run: Cheapest to build and cannot answer the only question that matters — why the number moved
  • RejectedPin billing only, read ownership and policy live: A reorganisation or a rule change silently rewrites closed periods
  • consideredEvent-sourced ledger of allocation decisions: Fully auditable and considerably more machinery; the snapshot model gets the same guarantee with a table and a hash
Consequences
What it buys
  • Any past figure can be reproduced exactly, years later, from data the platform still holds
  • 'Why did it change?' has a one-sentence answer: which of the three inputs moved
  • Restatement stops being an exception path and becomes the normal path executed again
  • A policy change can be simulated against a closed period, because the other two inputs can be held constant
What it costs
  • Ownership and policy must both be modelled as versioned, as-of-queryable data rather than current-state tables
  • Snapshot resolution adds a step and a failure mode to every run
  • Storage of superseded versions grows monotonically, bounded only by retention policy
Choose differently when
If the organisation were static, the provider bill immutable once issued, and the allocation rules fixed by regulation, the pinning machinery would be pure overhead and a current-state query would be correct. Nothing in a real cloud estate meets any of those three conditions.
Why it holds up over time
The contract — identical inputs produce identical output — is independent of the engine that enforces it. Spark, a warehouse or a technology that does not yet exist can each satisfy it, and every historical period stays reproducible across that change. This is the one decision in the set that should never need revisiting.
LessonWhen the inputs to a calculation are mutable and the output is something people act on, version the inputs rather than trying to stabilise them. Explaining a change is a design property, not a reporting feature.
Shown on views02 12 11
ADR-02

The bill is landed exactly as given, immutably, and normalised beside it

Accepted

Where does the platform's record of what the provider charged actually live, and can it survive a change to the way we interpret it?

Context
Provider billing schemas change: columns are added, SKUs are renamed, charge categories are retired, and the FOCUS specification itself is revised. A platform that transforms on ingest and keeps only the transformed result cannot re-derive history when its interpretation turns out to be wrong, and cannot prove to a disputing team what the provider actually said. The raw export is also the cheapest thing in the estate to store and the only thing that is genuinely irreplaceable at the point of ingestion, because a provider's detailed export is not indefinitely re-fetchable.
Decision
Every provider export is written to an immutable raw zone exactly as received, partitioned by provider, billing period and export version, with a manifest recording hash, row count and landing time. Normalisation to the conformed FOCUS cost record happens beside the raw form, never over it, and preserves provider-native fields alongside the conformed ones. Every downstream store is derived from the raw zone and can be rebuilt from it.
How it is realised on Azure
ADLS Gen2 with an immutable blob policy and a lifecycle rule moving partitions beyond 13 months to archive tiers. Data Factory writes the partition and the manifest in one activity; a partition is never rewritten, only superseded by a new export version. The conformer runs on Databricks and writes to the Azure Data Explorer fact store, which is explicitly a derived store.
Options weighed
  • ChosenImmutable raw zone as the source of everything: Cheap, complete, and the only thing that makes a re-interpretation of history possible
  • RejectedTransform on ingest, keep the conformed result only: Saves a few per cent of storage and makes every schema mistake permanent
  • RejectedRaw zone as a staging area, cleared after load: The common warehouse pattern; it removes the archive of record and the dispute evidence in one step
  • consideredKeep raw, but only for the current fiscal year: Reasonable if audit retention were shorter; here the statement retention of seven years sets the floor
Consequences
What it buys
  • A revised conformer re-derives all history instead of losing it
  • A disputing team can be shown exactly what the provider said, byte for byte
  • The raw zone doubles as the archive of record, which is what makes the fact store disposable (ADR-14)
  • Schema drift can be made a loud failure rather than a silent coercion
What it costs
  • 37 months of full-grain exports across three clouds is the second-largest storage line in the platform
  • Two representations of the same data must be kept consistent in meaning, if not in shape
  • Immutability makes deleting genuinely mis-delivered data an administrative exercise
Choose differently when
If a provider guaranteed indefinite re-fetch of detailed billing at original grain, the raw zone could become a cache rather than a record. No provider offers that today, and the detailed export retention windows are shorter than the seven-year statement retention this platform must meet.
Why it holds up over time
Billing formats will change repeatedly inside the life of this platform. Keeping the unmodified original is what turns each of those changes into a re-derivation rather than a discontinuity in the history.
LessonKeep the thing you were given, separately from the thing you made of it. The cost is storage; the alternative is that every interpretation error is permanent.
Shown on views09 10 13
ADR-03

Allocated facts are materialised per run, not resolved at query time

Accepted

Does attribution get computed once into a wide fact table, or resolved on demand from raw cost plus policy?

Context
This is the first genuinely open question in the requirement. Query-time resolution is appealing: policy iteration becomes nearly free, history can never be stale, and there is no multi-billion-row rewrite when a rule changes. But a statement that is computed on read has no physical existence, which means there is nothing to freeze, nothing to point at during a dispute, and no bound on the cost of a query over thirteen months. Materialising inverts every one of those trade-offs.
Decision
Each allocation run materialises allocated facts into a versioned fact store keyed by run_id. Dashboards read pre-aggregated roll-ups derived from those facts; drill-through reads the facts themselves. A policy change does not rewrite existing rows: it produces a new run, and statements supersede rather than update.
How it is realised on Azure
Azure Data Explorer holds allocated facts partitioned by billing period and run, with roll-up materialised views for the dashboard grain. A re-run writes a new run partition; the previous run's partition is retained until its statements are superseded and the 13-month allocated-fact retention expires.
Options weighed
  • ChosenMaterialise per run: Gives the statement a physical existence to freeze and bounds query cost
  • RejectedResolve attribution at query time: Free policy iteration, unbounded query cost, and nothing that can be frozen or disputed
  • consideredHybrid: materialise closed periods, resolve the current one live: Attractive, and it creates two code paths that will disagree — see ADR-12, which solves the same problem with one path and two labels
Consequences
What it buys
  • Dashboard latency is predictable over the full 13-month window
  • A statement is an artefact that exists and can be handed to an auditor
  • Drill-through is a read, not a recomputation, which is what makes the 10-second target achievable
What it costs
  • Every policy correction costs a full re-run of the affected periods, budgeted at 45 minutes p95
  • The fact store is the largest and most expensive store in the platform
  • Multiple runs of the same period coexist until retention expires
Choose differently when
If policy changed weekly rather than a few times a year, or if the estate were small enough that a full-grain query over 13 months were cheap, query-time resolution would win. At 1.2 billion records a month and a handful of policy changes a year, materialising is the cheaper side of the trade.
Why it holds up over time
The decision is really about where the statement lives, and statements are a governance artefact rather than a technology one. As query engines get faster the balance shifts, but the need for a frozen artefact does not.
LessonA number people will argue about needs a physical existence. Computation on read is elegant until someone asks you to produce the version they saw last month.
Shown on views03 09 12
ADR-04

Restatement supersedes, and is the normal path run a second time

Accepted

What happens when the provider changes an amount for a period that has already been published?

Context
Cloud billing is mutable for weeks: credits land late, commitment coverage is reallocated, usage arrives out of order, and providers issue corrections. Most platforms treat this as an exception and build a separate correction workflow, which then has its own bugs, its own permissions and its own divergence from the main path. The alternative is to notice that a restatement is nothing more than a new billing snapshot, and that a pure allocation function can simply be run again.
Decision
A re-issued export is landed as a new raw version, diffed at record grain into explicit revision rows, and — if the period is still open — absorbed silently. If the period is closed, the same allocation function is re-run against the new billing snapshot, producing a new statement version that supersedes rather than replaces its predecessor. Both versions remain retrievable for seven years, and a delta exceeding 1% or $1,000 of a team's period total notifies the owner within one business day.
How it is realised on Azure
The restatement detector compares the incoming export against the last ingested version by record key and emits revision rows into the conformed store. The restatement controller invokes the ordinary run orchestrator with the new billing snapshot id; the statement publisher writes version n+1 with a supersedes pointer in Azure SQL. The GL adjusting posting reuses the original idempotency key with a sequence suffix.
Options weighed
  • ChosenSupersede via a re-run of the same function: No second code path, and the correction is as reproducible as the original
  • RejectedIn-place correction of the published statement: Destroys the version somebody already read and reported
  • consideredCarry the delta forward into the next open period: What the platform does for restatements arriving past the 90-day window; wrong as a default because it misattributes the period
  • RejectedRefuse restatement after publication: Finance's number and the provider's invoice then diverge permanently
Consequences
What it buys
  • One code path, exercised every month, rather than an exception path exercised rarely and trusted blindly
  • The full history of what was believed when remains available for audit
  • A restatement's cause can be attributed, which is what feeds the close-loop review
What it costs
  • Statement storage grows with every restatement, retained for seven years
  • Downstream consumers must handle superseding versions rather than a single row per team and period
  • Compute for re-runs is a recurring, not exceptional, cost
Choose differently when
If providers issued immutable invoices with no post-issue corrections, restatement would be a genuine exception and a lighter mechanism would do. Within a 90-day window that is not the world any cloud customer lives in.
Why it holds up over time
Provider billing will remain mutable because the underlying commercial mechanisms — commitments, credits, negotiated discounts — are settled after the fact. Treating that as normal rather than exceptional is a property of the domain, not of this decade's tooling.
LessonIf something happens every month, it is not an exception. Build the main path so it can absorb it, rather than a second path that will rot.
Shown on views13 15 20

Ownership and attributionHow a cost record acquires an accountable owner, and what happens to the ones that do not.

ADR-05

Ownership is effective-dated and resolved by a declared precedence chain

Accepted

What is the authoritative statement that a resource belongs to a team, and how does it survive the organisation changing shape?

Context
Tags are granular, correctable by the team that owns the resource, and perpetually incomplete. The provider's own account hierarchy is enforced and reliable, but reorganising it is a migration project. An external mapping table is instantly correctable and instantly divergent from reality. In practice a large estate needs all three, and needs to know which one answered. It also needs to answer as of a date, because teams split, merge and dissolve continuously and a closed period's meaning must not change when they do.
Decision
Ownership resolves by a declared precedence chain — resource tag, then parent-scope tag, then account or subscription mapping, then an explicit exception entry — evaluated as of a point in time, with the rule that fired recorded on every resolved record. The whole model, including the org tree above it, is effective-dated: every assignment carries valid_from and valid_to.
How it is realised on Azure
Azure SQL holds ownership_assignment, team, cost_centre and product_line as effective-dated tables with as-of query support. The resolver runs as part of the ownership snapshot step and writes rule_fired and confidence onto each assignment. The exception registry is a first-class table with its own approval workflow, because entries in it override the cloud's own metadata.
Options weighed
  • ChosenPrecedence chain over effective-dated tables: Uses each source where it is strongest and records which one answered
  • RejectedTags only, enforced at creation: Correct in principle, and no large estate has ever achieved it retroactively
  • RejectedAccount hierarchy only: Reliable and far too coarse: a subscription rarely maps to one team
  • consideredExternal mapping table only: Easiest to correct and diverges from reality the moment nobody maintains it
  • RejectedCurrent-state ownership with no effective dating: A reorganisation then rewrites what every closed period meant
Consequences
What it buys
  • A closed period keeps its meaning across reorganisations
  • The rule that fired is visible, so a disputed attribution is arguable rather than mysterious
  • Metadata quality becomes measurable per source, which directs the coverage backlog
What it costs
  • Every ownership query is temporal, which complicates both the schema and the queries against it
  • The exception registry needs governance, because it can override the cloud's own truth
  • Four sources must be kept from contradicting each other in ways that confuse rather than resolve
Choose differently when
If tag policy could be enforced at resource creation across the whole estate and back-filled, the chain would collapse to one rule. That is worth pursuing and is not a reason to design for a state the estate is not in.
Why it holds up over time
The one thing certain to recur is organisational change. A model that stores ownership as of a time rather than as of now does not need rebuilding each time it happens.
LessonWhen several imperfect sources each know part of the answer, rank them explicitly and record which one answered. An unrecorded precedence chain is indistinguishable from a guess.
Shown on views11 09 22
ADR-06

A mid-period ownership change is effective-dated, not retroactive

Accepted

A resource changes hands on the fourteenth of the month. Who pays for the first thirteen days?

Context
Three answers are available and all three are defensible. Applying the change retroactively to the period start is trivial to compute and rewrites a statement someone may already have read. Deferring it to the next period is easy to explain and is straightforwardly wrong for a month. Splitting the period at the moment of change is correct and makes the ownership dimension time-varying in every query that touches it. The right answer depends on how often the organisation actually changes, and at 450 teams it changes constantly.
Decision
Ownership changes take effect from the moment recorded, and a period is split across the owners in force during it. Statements show the split explicitly as separate lines rather than a blended total, so that a team that owned a resource for half a month can see that it did.
How it is realised on Azure
ownership_assignment carries valid_from and valid_to; the allocation join is a temporal join between the cost record's charge_period_start and the assignment's validity interval. Where a change is recorded retrospectively, the effective date is the recorded business date, not the entry date, and the entry date is retained for audit.
Options weighed
  • ChosenEffective-dated, period split across owners: Correct, and honest about a real handover
  • RejectedRetroactive to period start: Simplest join and it rewrites history that has already been read
  • RejectedDeferred to the next period: Easy to explain and wrong for a whole month, every time
  • consideredEffective-dated with a same-day cut-off grace window: Reduces line-splitting noise; adds a rule with no principled boundary
Consequences
What it buys
  • Handovers are visible rather than smoothed away, which is what makes them negotiable
  • Closed statements never move because of a change recorded after they were frozen
  • The audit trail distinguishes when something became true from when it was recorded
What it costs
  • Every ownership join is temporal, and temporal joins are the most expensive part of the allocation run
  • Statements carry more lines, and a team that handed a resource over mid-month sees two partial entries
  • A retrospectively recorded change inside the correction window triggers a restatement (ADR-04)
Choose differently when
In an organisation that reorganised once a year, retroactive application would be simpler and almost always right. At 450 teams with continuous change, almost always is not often enough.
Why it holds up over time
The decision follows from effective dating (ADR-05) rather than standing alone, so it is as durable as that model is.
LessonWhen a fact has a time at which it became true and a time at which it was recorded, store both. Systems that store only one end up unable to explain either.
Shown on views11 15 22
ADR-07

The unallocated remainder is a named line charged to an accountable owner

Accepted

Some spend has no owner. Does the platform spread it, pool it, or charge it to somebody?

Context
Three strategies exist. Proportional spread balances the books to one hundred per cent and makes the gap invisible, which is precisely why tag coverage never improves in organisations that use it: nobody experiences the cost of bad metadata. A central pool keeps every team's number clean and quietly funds the mess out of a budget nobody defends. Charging it to a named owner creates pressure to fix the metadata, and also creates a person who will dispute the line every month. The choice is really about whether the platform exists to balance the books or to change behaviour.
Decision
The unallocated remainder is computed and published as its own line, in currency, in every view at every level of aggregation, before any absorption strategy is applied. The configured default strategy charges it to a named accountable owner — normally the platform or cloud-governance function — rather than spreading it. Teams see their own contribution to the remainder in a ranked reconciliation surface.
How it is realised on Azure
The remainder handler runs as the last step of the allocation run and writes remainder rows tagged with the reason (untagged, orphaned, unmatched egress, unresolved scope). The coverage scorer aggregates them per team, account and service, and the owner-facing reconciliation surface ranks them by cost so the most valuable fix is the obvious one.
Options weighed
  • ChosenNamed accountable owner, remainder always visible: Creates the pressure that actually improves coverage
  • RejectedProportional spread across allocated spend: Balances to 100%, hides the gap, and guarantees the gap persists
  • consideredCentral pool, unallocated: The honest interim position while coverage is still poor; it funds the mess without naming it
  • RejectedRefuse to publish until fully allocated: Publishes nothing for the first year and teaches nobody anything
Consequences
What it buys
  • The cost of bad metadata is felt by somebody who can act on it
  • Coverage becomes a measurable, ranked backlog rather than an aspiration
  • The 2% target is enforceable because the number is on every screen
What it costs
  • The named owner disputes the line, and that dispute recurs until coverage improves
  • Teams' totals do not sum to the estate total without the remainder line, which reads as untidy
  • It requires someone senior enough to be given an unpleasant number and expected to shrink it
Choose differently when
If tag coverage were consistently above 99%, the remainder would be noise and a proportional spread would be the pragmatic choice. Below that, spreading is how an organisation guarantees it never gets there.
Why it holds up over time
The incentive argument is independent of technology. As long as metadata is maintained by humans under deadline, visibility of the gap is what keeps it small.
LessonDo not make a measurement balance at the cost of making it useless. A visible gap is a backlog; a hidden gap is a permanent condition.
Shown on views09 16 20
ADR-08

Inferred attribution is marked, and excluded from chargeback

Accepted

The platform can often guess an owner from a naming convention or the identity that created a resource. Should it?

Context
Heuristic attribution is tempting because it lifts coverage quickly and the guesses are often right. It is also the fastest way to destroy trust: a team billed on the strength of a name pattern will find the one case where the pattern was wrong, and will then disbelieve every other number on the statement. But refusing to guess at all leaves a larger remainder than necessary and hides information the platform genuinely has.
Decision
The platform may infer an owner from a heuristic, but every inferred assignment is marked as inferred with a stated confidence, is shown as such in every view, and is excluded from chargeback postings. Inferred attribution counts towards showback coverage and is capped: no more than 2% of spend at close may be inferred.
How it is realised on Azure
ownership_assignment.confidence carries the inference strength and rule_fired names the heuristic. The GL posting builder filters on confidence = asserted. The dashboard renders inferred lines with an explicit marker rather than silently blending them into the team's total.
Options weighed
  • ChosenInfer, mark, exclude from chargeback: Uses the information without betting money on it
  • consideredNever infer: Purest, and it throws away a signal that genuinely narrows the remainder
  • RejectedInfer silently and treat as asserted: Buys coverage this quarter and credibility for ever
Consequences
What it buys
  • Coverage improves without the platform ever billing a guess
  • The marker itself is a prompt to fix the underlying tag
  • Showback and chargeback can legitimately report different completeness figures, with the reason visible
What it costs
  • Two coverage numbers must be explained: attributed, and attributed with confidence
  • The cap needs enforcement, or inference quietly becomes the primary mechanism
  • Heuristics need maintenance as naming conventions drift
Choose differently when
If chargeback were never going to be enabled, the distinction would carry no weight and inference could be treated as ordinary attribution. The requirement's explicit path to chargeback is what makes the separation load-bearing.
Why it holds up over time
The principle — do not bill a guess, but do not discard it either — is unaffected by how good the guessing gets. Better inference raises coverage; it does not change what may move money.
LessonMark estimates as estimates at the point they are created, not at the point somebody asks. A confidence field costs nothing and is impossible to retrofit honestly.
Shown on views09 11 16

Shared cost and commitmentsSplitting what nobody incurred alone, and deciding where a discount lands.

ADR-09

Shared compute is apportioned on max(request, usage), with idle to the platform owner

Accepted

A Kubernetes cluster costs what its nodes cost. Forty namespaces run on it. How is the bill split, and who pays for the headroom nobody used?

Context
Measured usage is fair to the bursty and rewards nobody for right-sizing: a team that reserves four cores and burns one pays for one. Declared requests reward right-sizing and make a team's bill predictable, but leave the reserved-but-unused capacity homeless, and that gap is frequently a third of the cluster. A blend is defensible and much harder to explain, and an apportionment nobody can follow is one nobody accepts. Underneath the arithmetic is a governance question: who is accountable for the cluster having more capacity than its tenants asked for?
Decision
Shared compute is apportioned on max(request, usage) per workload per interval, so a team pays for what it reserved or what it consumed, whichever is larger. The remaining cost — capacity that was neither requested nor used — is attributed to the platform owner who sized the cluster, as a named idle line rather than spread across tenants.
How it is realised on Azure
The usage collector streams per-pod CPU and memory requests and usage at 60-second resolution through Event Hubs, downsampled to five minutes on ingest. The apportioner joins those intervals to node cost from the conformed cost record for the same cluster and window, computes per-namespace shares, and emits the residual as an idle row owned by the platform team. A telemetry gap applies the last known-good ratio and marks the affected allocations degraded.
Options weighed
  • Chosenmax(request, usage), idle to the platform owner: Rewards right-sizing, and puts headroom on the only party that can change it
  • RejectedMeasured usage only: Under-charges reserved capacity and leaves a large unallocated residual
  • consideredDeclared requests only: Most predictable for tenants; ignores a team that consistently overruns its request
  • RejectedEven split across namespaces: Trivial to explain and actively rewards the heaviest tenant
  • RejectedIdle spread proportionally across tenants: Balances the cluster and removes the platform owner's incentive to size it correctly
Consequences
What it buys
  • Right-sizing has a visible financial reward for the team that does it
  • Cluster headroom becomes a line somebody owns and is asked about
  • The rule is explainable in one sentence, which is what makes it survive a negotiation
What it costs
  • Requires a second, high-volume telemetry stream that the provider bill does not supply
  • The platform owner receives an unpleasant number every month and will contest it
  • A telemetry gap degrades allocations, which excludes them from chargeback
Choose differently when
If clusters were provisioned exactly to aggregate requests with no headroom, idle would be negligible and pure request-based apportionment would be right. If tenants could not set requests at all, usage-only would be the only honest basis.
Why it holds up over time
The specific basis may be renegotiated; the structural claim — that the residual belongs to whoever chose the capacity — holds for any shared resource, including warehouses, queues and networks.
LessonApportion to the party who can act, not to the party who is easiest to charge. An allocation that nobody can respond to is a tax, not a signal.
Shown on views14 10 16
ADR-10

Commitment savings go to the consumer, with both amortised and cash views published

Accepted

A central team buys three years of reserved capacity. Whose bill gets the discount?

Context
Crediting the central buyer means teams see on-demand rates, over-estimate their own cost, and make architecture decisions against prices the organisation does not actually pay. Crediting the consuming workload produces a number that swings on a coverage allocation the team did not control and cannot predict. Publishing a fixed blended rate is stable and predictable and severs the link between a team's choices and its bill. Finance also needs the cash view for planning, which is a different shape from the view an engineer needs.
Decision
Commitment savings are attributed to the consuming workload at amortised rates, so teams see the price the organisation actually pays. Both an amortised view and a cash view are published for every period from the same ledger. Unused commitment coverage is held in a named central pool owned by the buyer, never spread across consumers.
How it is realised on Azure
The amortiser spreads prepaid and reserved purchases across the periods they cover and joins coverage to the underlying usage hours from the conformed cost record. Statements carry both amortised and cash totals as separate lines; the GL posting uses the amortised figure, since that is what the accounting policy recognises. Unused coverage is emitted as a central-pool row.
Options weighed
  • ChosenSavings to the consumer at amortised rates: Teams see real prices, which is what makes their architecture decisions correct
  • RejectedSavings to the central buyer: Teams optimise against list prices the organisation never pays
  • consideredFixed published blended rate: Most predictable for a team; hides which workloads are actually covered
  • RejectedCash view only: A three-year prepayment lands entirely on one month and one team
Consequences
What it buys
  • Architecture decisions are made against prices the organisation actually pays
  • Finance gets its cash view without a separate pipeline or a separate truth
  • Unused coverage is visible to the person who bought it, which is how buying improves
What it costs
  • A team's unit cost moves when central coverage is reallocated, through no action of its own
  • Two totals per period must be explained and never confused
  • Amortisation schedules add state that must survive a commitment being exchanged or refunded
Choose differently when
If commitments were bought by the consuming teams themselves rather than centrally, the attribution question largely disappears. If coverage churned so often that team unit costs became unpredictable, a published blended rate would be the more useful lie.
Why it holds up over time
Commercial mechanisms will keep changing name — reserved instances, savings plans, committed use, flexible commitments — but the structural question of where a centrally purchased discount lands does not.
LessonShow people the price they actually pay, even when it is harder to compute. Optimising against a price nobody pays produces decisions nobody wants.
Shown on views14 11 09

Publication and disputesFreezing, restating, disputing and posting — the lifecycle of a number people act on.

ADR-11

Reconciliation to the provider invoice gates publication

Accepted

The ingested total does not match the provider's invoice. Does the platform publish anyway and flag it?

Context
A variance means one of several things: an export is incomplete, a charge category was dropped in conformance, currency conversion is wrong, or the provider issued something the pipeline does not understand. None of these is knowable in advance, and all of them make the per-team numbers wrong in ways that are not visible per team. Publishing with a warning banner feels cooperative and, in practice, means the wrong numbers are read, quoted and acted on, and the banner is ignored by the second month.
Decision
A period whose ingested total differs from the provider's invoice or billing summary by more than 0.1% or $500, whichever is larger, does not publish statements for that provider and period. Showback may be late; it may not be quietly short. The variance is escalated with the affected provider, period and magnitude named.
How it is realised on Azure
The invoice reconciler runs after conformance and writes a reconciliation result per provider and period. The statement publisher reads it as a precondition. A blocked provider publishes the period as visibly incomplete, naming the provider, rather than publishing a total that omits it.
Options weighed
  • ChosenBlock publication on variance: Being late is recoverable; being quietly wrong is not
  • RejectedPublish with a warning banner: Banners are read once and ignored thereafter
  • consideredPublish and post an adjusting line for the variance: Reasonable when the variance is understood; useless when it is not, and it is not by definition
  • RejectedReconcile asynchronously after publication: Guarantees the wrong number gets quoted before the right one exists
Consequences
What it buys
  • A published statement has a provable relationship to what the provider actually charged
  • Variance investigation happens before the number is read, not after it is disputed
  • The reconciliation result is itself a retained artefact on the statement
What it costs
  • A single misbehaving provider can delay a whole close
  • The tolerance is a judgement call and will be argued over
  • It puts an operational dependency on a provider API that is outside the platform's control
Choose differently when
If per-provider statements were acceptable to finance, a variance could block one provider without delaying the rest of the close — which is in fact what the design does at provider granularity. A single blended statement would make the blocking cost much higher.
Why it holds up over time
For any system whose output is a number people act on, correctness gates beat correctness metrics. That holds regardless of what the pipeline is built from.
LessonDecide in advance whether being late or being wrong is worse, and encode the answer as a gate. Systems that leave it to a dashboard have chosen 'wrong' without saying so.
Shown on views12 19 05
ADR-12

One pipeline, two labels: the current period is an estimate, never a statement

Accepted

Engineers need today's number; finance needs a number that does not move. Is that one system or two?

Context
The current period is provably incomplete: usage is still arriving, amounts are still being restated, and commitment coverage has not been applied. Serving it from a separate fast path gives engineers a daily number and guarantees it will disagree with the monthly statement that follows, at which point both are disbelieved. Serving it from the same path raises the risk that a reader cannot tell which kind of number is on screen — which is the same failure by a different route.
Decision
Current-period figures are produced by the same pipeline and the same allocation function as closed periods, from the partial billing snapshot available at the time, and are labelled as estimates everywhere they appear. An estimate and a frozen figure are never combined into one number, never appear in the same chart series without distinction, and the estimate carries the timestamp of the snapshot it came from.
How it is realised on Azure
The daily incremental run writes allocated facts tagged with period_state = open. The serving layer renders open-period figures with an explicit estimate marker and the snapshot time; the statement viewer refuses to render an open period at all. Export and API responses carry period_state so downstream tooling cannot lose the distinction.
Options weighed
  • ChosenOne pipeline, explicit labels: One set of bugs, and the distinction is carried in the data rather than in the reader's memory
  • RejectedSeparate fast estimate path: Two truths that will disagree, and the disagreement destroys both
  • RejectedClosed periods only, no current view: Makes the platform useless for the journey that carries its value
  • consideredCurrent view from raw provider totals, unallocated: Cheap and fast, and it cannot answer 'is it my team' — which is the whole question
Consequences
What it buys
  • The daily number and the monthly number are produced the same way, so they converge rather than diverge
  • Anomaly detection runs on the same allocated facts, so an alert already has an owner
  • There is only one allocation implementation to test, tune and trust
What it costs
  • Every surface must carry and render the estimate distinction, including exports and API responses
  • Daily incremental runs add compute that a month-end-only design would not need
  • An estimate that moves materially before close still generates questions, even when correctly labelled
Choose differently when
If the organisation only ever consumed monthly figures, the current-period path could be dropped entirely and the design would simplify considerably. The anomaly-detection requirement alone makes that untenable.
Why it holds up over time
The tension between fresh and final is permanent in any system over late-arriving data. Solving it with labels rather than with a second pipeline is what keeps the two from drifting apart over years.
LessonWhen two audiences want different properties from the same number, change the label and the contract, not the pipeline. Two pipelines become two truths faster than anyone expects.
Shown on views12 16 04
ADR-13

Disputes never block publication, and close by correction or reasoned rejection

Accepted

A team says their line is wrong. What happens to the close?

Context
Allowing a dispute to hold publication gives any team a veto over the whole organisation's close, and at 450 teams that veto will be exercised. Resolving disputes by editing the published figure destroys the reproducibility the whole architecture is built on. And closing a dispute by simply declining it, with no recorded reason, guarantees the same dispute returns next month and the month after.
Decision
A dispute is recorded against a specific statement version and never blocks publication. It closes in exactly one of two ways: a correction, which is a restatement producing a superseding version (ADR-04), or a reasoned rejection, which records why the figure stands. Both outcomes are retained with the statement for seven years.
How it is realised on Azure
The dispute ledger in Azure SQL holds dispute rows keyed to statement_id and version, with state, raiser, resolution and reason. The statement service exposes the dispute alongside the line it concerns, so a reader of v1 sees that it was contested and how that ended.
Options weighed
  • ChosenNon-blocking, closed by correction or reasoned rejection: The close completes and the disagreement is still on the record
  • RejectedDispute blocks publication of the affected statement: Hands every team a veto over the organisation's close
  • RejectedResolve by editing the figure in place: Destroys reproducibility to save a version row
  • consideredTrack disputes in the ticketing system instead: Less to build; the dispute then is not attached to the version it concerns and is lost to the audit trail
Consequences
What it buys
  • The close is predictable regardless of how many disputes are open
  • Rejections are recorded, so the same argument does not recur every period
  • Dispute causes are classifiable, which is what feeds the policy and coverage backlog
What it costs
  • A team may act on a figure that is later superseded
  • Someone must own reasoned rejection, which is an unpopular job
  • Dispute volume becomes an operational load that scales with the number of teams
Choose differently when
In a small organisation where a dispute is rare, blocking publication until it is resolved would be reasonable and simpler. At 450 teams it is a guaranteed deadlock.
Why it holds up over time
Disagreement about an allocated number is structural, not transitional: the rules encode a negotiation. A workflow that assumes disputes are temporary will be rebuilt within two years.
LessonRecord the rejections as carefully as the corrections. An organisation's memory of why a decision stands is what stops it being relitigated every cycle.
Shown on views15 05 20

Scale, storage and costWhat is kept, for how long, at what grain, and what the platform is allowed to cost.

ADR-14

Full grain is kept in the raw zone; allocated facts are kept 13 months and are disposable

Accepted

At 1.2 billion cost records a month growing 40% a year, what is actually retained, at what grain, and for how long?

Context
Full-grain hourly resource-level detail is the only thing that answers 'which resource, on which day' during a dispute, and it is simultaneously the dominant cost and the dominant scale problem in the platform. Rolling up at ingest makes the platform cheap and makes some disputes unanswerable. The useful observation is that the raw zone already holds complete, immutable, full-grain detail on cheap tiered storage, which means the expensive query-optimised store does not have to be the archive of record.
Decision
Raw exports and conformed cost facts are retained 37 months, tiering to archive beyond 13. Allocated facts are retained 13 months at full grain and are explicitly derived and disposable, rebuildable within 24 hours from the raw zone plus the pinned ownership and policy versions. Published statements, disputes and audit are retained seven years. Container usage telemetry is kept 90 days at native resolution and 13 months downsampled hourly.
How it is realised on Azure
ADLS Gen2 lifecycle rules move raw partitions to cool and then archive; Azure Data Explorer retention drops allocated partitions past 13 months; the statement and audit stores in Azure SQL are geo-replicated and never pruned inside the retention window. A historical query beyond 13 months is served by re-running allocation for that period rather than by keeping the rows hot.
Options weighed
  • ChosenFull grain in the raw zone, 13 months hot for allocated facts: The cheap immutable store is the archive; the expensive store holds only what is queried
  • Rejected37 months of allocated facts hot: Triples the largest cost line to serve queries that are almost never made
  • RejectedRoll up at ingest beyond 90 days: Makes historical disputes unanswerable and cannot be undone
  • RejectedKeep everything hot and revisit later: A measurement platform that cannot meet its own cost target has an obvious credibility problem
Consequences
What it buys
  • The platform's own cost stays inside the 0.5% target it publishes about itself
  • A retention decision remains reversible, because nothing irreplaceable lives in the store being pruned
  • Historical questions are answerable, just not instantly
What it costs
  • A query older than 13 months takes a re-run rather than a read, measured in tens of minutes
  • The rebuild path must be exercised regularly or it will not work when needed
  • Two retention windows must be explained to auditors and to teams
Choose differently when
If historical full-grain queries turned out to be frequent rather than rare, or if storage costs fell far enough that the distinction stopped mattering, keeping the full 37 months hot would be simpler. Instrumenting how often deep history is actually retrieved is the way to find out.
Why it holds up over time
The structural insight — that the immutable source can serve as the archive, freeing the query store to hold only what is queried — survives any change in storage pricing. Only the boundary moves.
LessonRetention is not one decision. Separate what must be kept from what must be fast, and the expensive store gets much smaller.
Shown on views10 17 09
ADR-15

Serving and batch are separate tiers, and batch runs on interruptible capacity

Accepted

Allocation runs are large, bursty and not latency-critical. Dashboards are small, constant and latency-critical. Do they share infrastructure?

Context
Sharing one elastic pool is operationally simpler and means a month-end allocation run competes with an engineering lead trying to open a dashboard on the same morning — which is exactly the morning the dashboard matters most. Separating them costs a second deployment and a second set of scaling rules. Because no allocation run is latency-critical inside its window, the batch tier can also take interruptible capacity, which is the single largest saving available to a platform that must justify its own cost.
Decision
Serving and batch are separate tiers with separate scaling. The serving tier runs zone-redundant and autoscaled on demand; the batch tier runs on spot capacity with per-partition checkpointing, so an interruption re-runs a partition rather than a period. No interactive request ever waits on an allocation run.
How it is realised on Azure
AKS with separate node pools and workload scheduling constraints for serving; Databricks job clusters on spot instances with on-demand fallback for the driver. Allocation partitions are keyed by provider and day, checkpointed on completion, so a lost worker resumes from the last committed partition.
Options weighed
  • ChosenSeparate tiers, spot batch: Protects the latency path and takes the largest available saving
  • RejectedOne shared elastic pool: Simplest, and it degrades the dashboard exactly when it is most read
  • consideredSeparate tiers, on-demand batch: Removes interruption handling; pays a large premium for a workload with a long window
  • consideredServerless query engine for both: Attractive operationally; costs of a full-grain monthly re-run become hard to bound
Consequences
What it buys
  • Dashboard latency is unaffected by close-week batch load
  • Batch compute cost falls substantially, which is what makes the 0.5% target reachable
  • Interruption handling forces partition-level idempotency, which the re-run model needs anyway
What it costs
  • Two deployment topologies, two sets of scaling rules, two on-call surfaces
  • Spot reclamation lengthens the tail of a run, so the 45-minute p95 carries a 90-minute ceiling
  • Checkpointing adds write amplification to every partition
Choose differently when
If allocation became latency-critical — a real-time showback requirement, say — the batch tier would need on-demand capacity and the whole cost argument would change. Nothing in this requirement asks for that.
Why it holds up over time
The separation of a latency-critical read path from a throughput-oriented compute path is one of the oldest stable patterns there is, and nothing about cloud billing is likely to dissolve it.
LessonInterruptibility is cheap when the work is already idempotent. Designing for a pure, re-runnable function bought the spot discount for free.
Shown on views07 17 12

Access and confidentialityWho may see whose bill, and why estate-wide cost data is sensitive.

ADR-16

Scope is resolved server-side from the org tree; out-of-scope reads are answered at aggregate grain

Accepted

Cost data discloses architecture, headcount, vendor terms and roadmap timing. Who may see whose, and what happens when someone asks for more?

Context
Estate-wide cost detail at resource grain is one of the more revealing datasets an organisation holds: it exposes which products are growing, which teams are large, what was negotiated with which vendor, and when something new started being built. At the same time, a platform that refuses every cross-team question drives people to exported spreadsheets, which is strictly worse for confidentiality than answering carefully. Scope also has to be temporal, or a reorganisation retroactively opens or closes access to closed periods.
Decision
Read scope is resolved server-side from the effective-dated org tree as of the period being read, and injected into every query as a predicate that a request parameter cannot widen. A team lead sees their team's detail, a product owner their line, finance and FinOps the estate. An out-of-scope read is answered at aggregate grain — totals without resources — rather than refused, and every cross-team read is logged. Write access to ownership and to allocation policy is a separate role from read access.
How it is realised on Azure
Entra ID issues group claims naming the person; the scope resolver derives the team set from the org tree rather than from the claim, so a group membership cannot itself grant cost visibility. The query API injects the predicate before execution and rejects any attempt to override it. Audit rows are written for policy changes, ownership changes, publication, restatement, dispute actions, bulk exports and cross-team reads, retained seven years.
Options weighed
  • ChosenServer-side scope from the org tree, aggregate answer out of scope: Least privilege without pushing people into spreadsheets
  • RejectedHard denial outside scope: Teaches nothing and creates a strong incentive to exfiltrate
  • RejectedScope from IdP group claims: Makes cost visibility a side effect of group membership nobody is reviewing for that purpose
  • consideredOpen estate-wide read for all engineers: Excellent for a culture of ownership; unacceptable for vendor terms and pre-announcement workloads
Consequences
What it buys
  • Confidential detail stays with the people accountable for it
  • Benchmark questions are answerable without disclosing another team's architecture
  • Scope is correct for closed periods even after a reorganisation
What it costs
  • Every query carries a temporal scope resolution, which adds latency and complexity
  • The aggregate-answer rule needs care so that repeated narrow queries cannot reconstruct detail
  • A separate admin role for ownership and policy means more roles to govern
Choose differently when
In an organisation with a genuinely open cost culture and no vendor-confidentiality constraints, estate-wide read would be simpler and better for behaviour. Vendor terms and unannounced products are what remove that option here.
Why it holds up over time
What cost data reveals does not change with technology. As long as spend correlates with headcount, product investment and vendor negotiation, it will be commercially sensitive.
LessonAnswer the question at a grain you can safely disclose, rather than refusing it. A refusal does not remove the need; it removes your visibility into how it gets met.
Shown on views21 22 10

Every package used, in one table

The terms below are used precisely in this package. Several of them are used loosely in the wider FinOps literature, and the difference matters when reading the decision records.

PackageWhat it isWhat it does hereConsidered instead
Showback Reporting allocated cost to an accountable owner for information, without moving budget. The platform's initial operating mode; the ledger must be trusted here before it is allowed to bill. Chargeback, where the same figure moves real budget through the general ledger.
Allocation The attribution of a cost record to an accountable owner, including any apportioned share of a shared resource. The pure function at the centre of the architecture, over three pinned inputs. Tagging, which is one of several inputs to allocation rather than a synonym for it.
Apportionment Splitting the cost of a resource nobody incurred alone across the parties that consumed it, by a declared basis. Applies to clusters, warehouses, egress and fixed fees; the basis is per pool and versioned in policy. Direct attribution, where a single owner is identifiable without a split.
Amortisation Spreading a commitment purchase across the periods it covers, rather than the period it was paid in. Produces the amortised view engineers should optimise against; the cash view is published alongside. Cash view, which recognises the payment when it was made.
Restatement A re-issued provider export changing amounts for a period, and the superseding statement version that follows. Handled as the normal path run a second time, not as an exception workflow. Correction in place, which this architecture does not permit.
Statement A frozen, versioned artefact per team and period, carrying the three input versions that produced it. The unit of publication and the thing a dispute is raised against. The estimated current-period view, which is never a statement.
Remainder Spend with no resolved accountable owner at the point of allocation. Published as its own line at every level before any absorption strategy is applied; target ≤ 2%. Proportional spread, which balances to 100% and hides the same quantity.
FOCUS The FinOps Foundation's open specification for a common cloud billing record. The conformed internal cost record, with provider-native fields retained alongside. A bespoke internal billing schema.
Effective dating Storing a fact with the interval during which it was true, separately from when it was recorded. Applies to ownership, the org tree and policy, and is what lets a snapshot be taken as of any instant. Current-state modelling, where a reorganisation rewrites the meaning of closed periods.
Run digest A hash over the three pinned input ids plus the engine version. Makes an allocation run idempotent and proves that two results came from identical inputs. A run timestamp, which proves only when something happened.
Unit economics Allocated cost divided by a team-reported business driver, at the driver's own grain. Separates volume change from efficiency change, which a raw cost trend cannot. Absolute spend trend, which cannot distinguish growth from waste.
Idle Shared capacity neither requested nor consumed by any tenant in an interval. Attributed to the platform owner who sized the resource, as a named line. Overhead spread, which averages it across tenants who cannot act on it.
Open svg/<view>.svg or drawio/<view>.drawio in draw.io Desktop or at app.diagrams.net to edit. The SVG carries the diagram inside it, so it is both the picture and the source. This folder is self-contained — copy it whole and every link still resolves.