Case Study 1: The Week Spent on One Question
"We thought we were arguing about a bridge table. We were arguing about what a discount is, and three departments had different answers."
Executive Summary
While modeling fct_order_item, Kestrel's data team hit a question that looked clerical: does a
promotion apply to an order, or to an order line? The answer determines whether the model needs a
bridge table, which is a modeling decision worth perhaps an hour.
It took a week, because the question turned out to be a proxy for a business disagreement that had been latent for three years. Merchandising, finance, and growth each had a working definition of "discount," each was internally consistent, and each produced a different revenue figure. The existing reports had been quietly using all three.
This case study is about modeling as an instrument for finding organizational disagreement. The schema at the end is unremarkable — a bridge table with an allocation weight, exactly as §6.7 describes. The value is in how the disagreement surfaced, how it was resolved, and what the team did so the resolution would stick.
Skills applied: the four-step process and grain declaration (§6.3); bridge tables and allocation weights (§6.7); the fan trap (§6.9); definitional resolution (§6.8).
Background
The trigger. The team was at step 3 of the four-step process for order placement — identifying
dimensions. dim_promotion was on the list and looked routine. The question was where the key goes.
If a promotion applies to an order, then promotion_key belongs on fct_order, one per row, and
fct_order_item gets it by inheritance.
If a promotion applies to a line, then promotion_key belongs on fct_order_item. And if
more than one promotion can apply to one line, you need a bridge table.
What the source data said. They looked. order_items.discount_cents was a single number per
line, with no reference to any promotion. promotions existed as a table. There was no join between
them anywhere in the source schema.
The discount had been applied by the checkout service and the promotion that caused it had not been recorded.
⚠️ Failure Mode — The number without its reason
discount_centsrecords that a discount was applied and not why. The source system had no reason to record why: the checkout service computes a price, charges it, and moves on. The reason was operationally irrelevant to the system that produced the number.This is one of the most common shapes of data problem, and it is genuinely nobody's fault. A source system records what it needs to do its job, and analytics almost always needs one more column. The checkout service was correct not to store what it did not use.
The consequence is that the question "which promotions actually drive revenue" was unanswerable from historical data and would remain so until the source started recording it. Three years of promotional history existed only as an undifferentiated
discount_cents.Two things fall out of this, and the second is the one people miss:
- The fix is upstream. The checkout service must emit the promotion identifier. That is a product engineering change, it takes a quarter to land, and there is no data engineering substitute for it.
- Ask early, for every metric you expect to want. The cost of adding a column to a source system's event is small at design time and enormous retroactively, because the past cannot be regenerated. Chapter 17's data contracts are the mechanism for having this conversation before rather than after.
The Problem
With the source unable to answer, the team went to the business. That is where the week went.
Merchandising's definition. A promotion is a campaign — "20% off outerwear, November 3–10." It applies to products, so its effect is naturally per line. They wanted to know: for each campaign, what was the incremental revenue on the products it covered?
Finance's definition. A discount is a reduction in the amount collected, full stop. It applies to an order, because that is what the customer is invoiced for and what the payment processor settles. Splitting an order-level discount across lines is, in their words, "making up a number."
Growth's definition. A promotion is anything that changed the customer's decision — including a personal code, a free-shipping threshold, a first-order incentive, and a loyalty tier discount. They wanted attribution per customer per campaign, and they considered free shipping a promotion, which neither of the other teams did.
Three definitions, three grains, three scopes. And every one of them was in use somewhere:
| Report | Definition in use | Owner |
|---|---|---|
| Campaign performance dashboard | Merchandising's | Merchandising analyst |
| Monthly revenue reconciliation | Finance's | Controller |
| Cohort payback model | Growth's | Growth analyst |
| Executive weekly summary | Mixed — used the finance figure for revenue and the merchandising figure for discount | nobody |
That last row is the one that made the meeting go badly. The executive summary was computing a discount rate by dividing merchandising's discount by finance's revenue — two different scopes over two different grains — and the resulting percentage had been quoted in a board deck.
The Analysis
The team did three things, in an order worth copying.
1. They stopped arguing about the schema
Two days in, the discussion was about whether to use a bridge table. The manager stopped it and pointed out that the schema question could not be answered until the definition was.
This is the single most useful intervention in the case study. Schema arguments are comfortable — they are technical, bounded, and everyone can participate. Definitional arguments are uncomfortable and require someone to concede. Teams drift from the second to the first without noticing, and the tell is that the technical discussion keeps going in circles.
2. They enumerated discount types from the data
Rather than asking teams what promotions existed, they measured what the data contained. One query against three years of orders, bucketing by discount characteristics:
| Discount pattern | Share of discounted orders | Applies at |
|---|---|---|
| Percentage off specific products | 41% | Line |
| Percentage off entire order | 22% | Order |
| Fixed amount off order (code) | 18% | Order |
| Free shipping threshold | 11% | Order |
| Loyalty tier standing discount | 6% | Order |
| Employee discount | 2% | Order |
59% of discounts apply at order level and 41% at line level. Neither definition was a minority position, which is exactly why the disagreement had persisted — both sides had most of a case.
That measurement reframed the conversation from "who is right" to "we clearly have two things and have been calling them one."
3. They separated the model from the metric
The resolution came from noticing that the three teams needed different metrics, not different models. A model that records what happened at the finest available grain can serve all three; a model that bakes in one team's definition can serve only that team.
The Decision
The model:
fct_order_item bridge_order_item_promotion dim_promotion
order_item_id ─────────────▶ order_item_id promotion_key
... promotion_key ◀───────────────────── code
discount_cents allocation_weight DECIMAL(9,8) kind
allocated_discount_cents scope: 'line'|'order'
starts_at, ends_at
In words: order lines join to promotions through a bridge table that carries an allocation weight
and the allocated discount amount. dim_promotion records the promotion's scope — whether it
was natively a line-level or order-level promotion — which is the column that lets each team filter
to the definition it means.
The allocation rule, written down, for order-level promotions split across lines:
An order-level discount is allocated to lines in proportion to each line's
extended_price_centsas a share of the order's totalextended_price_cents, rounded to the cent, with any rounding remainder assigned to the largest line so that allocated amounts sum exactly to the order-level discount.
That last clause — the remainder rule — is not decoration. Without it, allocated line discounts sum to within a cent or two of the order discount, and a reconciliation to the cent fails on roughly one order in three.
Three metrics, defined once each:
| Metric | Definition | Whose |
|---|---|---|
line_discount_cents |
Sum of allocated discount where dim_promotion.scope = 'line' |
Merchandising |
order_discount_cents |
Sum of discount at order grain, from fct_order |
Finance |
total_discount_cents |
Sum of all allocated discount, any scope | Growth |
Each is a named metric in the semantic layer. Nobody says "discount" without a prefix, and the
executive summary was rebuilt to use total_discount_cents over net_revenue_cents, both from the
same grain.
📐 Design Decision — Allocate, or refuse to allocate?
The team seriously considered not allocating order-level discounts to lines, keeping them only in
fct_order, and telling merchandising that line-level discount analysis simply excludes order-level promotions.The case for refusing: an allocation is a fabricated number. Finance's objection was substantive — nobody decided that this line bore $2.14 of the $10 order discount; a rule decided it. Once allocated, that fabricated number will be summed, charted, and quoted by people who have no idea a rule produced it.
The case for allocating: 59% of discounts are order-level. Excluding them from line analysis means merchandising's campaign view is missing most of the discounting, which makes it misleading in a different and less visible way.
What decided it: allocation was chosen and made explicit. The bridge table carries
allocation_weightas a visible column rather than only the allocated amount, so anyone inspecting a row can see that a rule was applied and what it was.dim_promotion.scopelets any consumer exclude allocated amounts entirely.What that costs: every consumer now has a choice to make, and some will make it wrong. The mitigation is that the three named metrics make the common cases correct by default — a consumer has to go out of their way to build the wrong thing.
The generalizable rule: if you must fabricate a number, make the fabrication visible in the data rather than only in the documentation. A weight column beside the allocated amount is worth more than a paragraph nobody reads.
What Happened
The model shipped in three days once the definitions were settled. The week was the definitions.
The upstream fix took two quarters. Product engineering added promotion_id to the checkout
event and to order_items, which meant that from 2025-09 onward, promotions were recorded rather
than inferred. Everything before that date uses a heuristic reconstruction — matching discount
patterns to promotion validity windows — which is documented, imperfect, and flagged in the model
with a promotion_attribution_method column reading 'recorded' or 'inferred'.
That flag column is the most-praised part of the design, according to the team's own retrospective. Analysts can exclude inferred attribution when precision matters. Nobody has to remember which date the change happened.
The executive summary's discount rate changed by 4.2 percentage points when it was rebuilt on consistent definitions. That conversation was uncomfortable and it was had once, rather than being had implicitly every quarter.
Two follow-on effects:
The allocation remainder rule was tested (SUM(allocated_discount_cents) per order equals
fct_order.order_discount_cents exactly) and the test failed twice in the first month — both times
on orders with a single line where the code took a shortcut, and both times caught before anyone saw
a wrong number.
Six months later, a new promotion type — a buy-two-get-one offer — did not fit the allocation rule,
because the discount is conceptually on a specific line rather than proportional. The scope
column made room for a third value, 'line_specific', without restructuring anything. A model that
records a rule's identity can accommodate a new rule; a model that has a rule baked into a number
cannot.
Lessons
-
Modeling surfaces organizational disagreement. The question was "bridge table or not." The answer required three departments to agree on what a discount is, and they had not for three years.
-
When a technical argument goes in circles, check whether it is a proxy. Schema debates are comfortable; definitional debates require someone to concede. Teams drift from the second to the first without noticing.
-
Measure the data before asking the business. 59/41 was the fact that reframed the argument from "who is right" to "we have two things and one word."
-
A source system records what it needs, and analytics always needs one more column. The promotion that caused a discount was never recorded because checkout did not need it. Ask early; the past cannot be regenerated.
-
Separate the model from the metric. One model at the finest grain serves three definitions. A model with one definition baked in serves one.
-
If you must fabricate a number, make the fabrication visible in the data. The
allocation_weightcolumn and thescopecolumn are worth more than documentation. -
Write the remainder rule. Allocation without an explicit rounding rule fails a reconcile-to-the-cent test on a large fraction of rows.
-
A method flag beats institutional memory.
promotion_attribution_methodmeans nobody has to remember which date the upstream fix landed.
Questions for Discussion
-
The executive summary divided merchandising's discount by finance's revenue. How would you find other instances of this pattern — a metric assembled from two incompatible sources — across twenty-two dashboards?
-
The team spent two days on the schema before the manager redirected them. What would have caught it sooner? Is there a question that reliably distinguishes a technical disagreement from a definitional one?
-
Finance objected that allocation is "making up a number," and they were right. Construct the strongest version of the refuse-to-allocate position and say what merchandising would have had to accept.
-
The pre-2025-09 promotion attribution is inferred by matching discount patterns to validity windows. Design that heuristic. Then estimate its error rate and say how you would measure it.
-
promotion_attribution_methoddistinguishes recorded from inferred data. Where else in a data platform would a method flag like this be valuable? What is the cost of adding them liberally? -
The upstream fix took two quarters. What could the data team have offered product engineering to make it faster, and at what point does pushing for upstream change become someone else's prioritization problem rather than yours?
-
The discount rate in the board deck changed by 4.2 points. Write the two paragraphs explaining that change to an executive audience. What do you lead with?