What architecture decision cost actually includes
The architecture decision cost is everything the choice will consume over its lifetime, not just the effort to build it. A useful model splits it into five categories, because each one is paid by a different budget and becomes visible at a different time.
| Cost category | Timing | Typical source of the number | Commonly missed because |
|---|---|---|---|
| Build and migration effort | One-time | Sprint estimates | Migration tail is underestimated |
| Infrastructure spend | Recurring | Cloud bill, vendor quote | Rarely missed |
| Operational load (on-call, upgrades, patching, capacity planning) | Recurring | Rarely tracked | Spread across many people in small slices |
| Cognitive and onboarding cost | Recurring | Not tracked | Shows up as slower ramp-up, not as a line item |
| Exit or reversal cost | Deferred | Not estimated | Nobody plans to leave |
Infrastructure spend is the easiest number to get because an invoice already exists. That is also why it dominates discussions, even when it is not the largest cost. A cluster that costs little to run but needs a few hours of engineer attention every week can cost more than a pricier managed service. Labour is usually the most expensive input in the system.
Behind the operational line sits opportunity cost. Every hour spent patching brokers or resizing disks is an hour not spent on product work. That cost never appears on a bill, but it is real.
Some costs resist honest quantification. Record them as qualitative line items instead of inventing a figure:
- Hiring difficulty for niche technology
- Morale and attrition risk under heavy on-call
- Concentration risk when only one or two people understand the system
Who should build a cost model, and when it is worth the effort
The people who benefit most are the backend and platform engineers proposing or reviewing a decision, and the tech lead who approves it. The proposer needs the model to see the hidden costs. The reviewer needs it to challenge specific assumptions instead of debating conclusions. The approver needs it to compare options on the same terms.
Decide how much to model based on how expensive the decision is to reverse. Swapping a client library or changing a cache TTL is cheap to undo. For these, a one-hour spreadsheet is enough, and often more than enough. Over-analyzing a reversible choice can cost more than picking an option and changing course later.
Reserve deeper modeling for decisions that are costly to unwind, especially those touching data storage, tenancy boundaries, or core protocols. Data has gravity. Tenancy shapes every query and permission check. Protocols leak into every client. Mistakes in these areas compound as more code depends on them.
When teams skip an explicit model, they usually fall back on one of these alternatives:
| Alternative | What it captures | What it misses |
|---|---|---|
| Gut feel or senior judgment | Pattern recognition from past systems | Assumptions stay implicit and can't be reviewed or recalibrated |
| Pure infra-price comparison | Visible line-item spend | Engineering hours, on-call load, migration and reversal cost |
| Qualitative pros/cons list | Breadth of considerations | Relative weight; ten minor pros can appear to outweigh one major cost |
An explicit model doesn't replace judgment. It writes judgment down so the next decision can learn from it.
How the model works: hours, loaded cost, horizon, and ranges
The model reduces every option to one number by converting engineering time into money and adding it to spend. For each option, estimate monthly hours in two buckets: routine toil (upgrades, patching, capacity changes) and incident work (paging, diagnosis, follow-ups). Multiply total hours by a loaded hourly cost, meaning salary plus benefits, overhead, and on-call compensation. Add monthly infrastructure spend, multiply by the number of months in the horizon, and add the one-time build or migration cost:
TCO = build_cost + horizon_months × ((toil_hours + incident_hours) × loaded_rate + infra_monthly)
A fixed horizon, such as 36 months, lets you compare an option with high upfront cost and low recurring cost against one with the opposite profile.
Single-point inputs hide uncertainty, so give each input three values: low, likely, and high. Then vary one input at a time across its range while holding the others at "likely," and record how much the total moves. Usually one or two inputs, often incident hours, account for most of the spread. Reviewers can then argue about whether incident hours are 5 or 20 a month, which is a question they can check, instead of arguing about which option "feels" cheaper.
Two caveats apply:
- The horizon choice biases the result. Short horizons favor options that are cheap to build. Long horizons favor options that are cheap to run. State the horizon and explain why you chose it.
- A single loaded rate treats hours as interchangeable. An hour of senior incident response at 3 a.m. displaces planned work and wears down the people doing it. Consider weighting incident hours more heavily than toil hours.
# Illustrative estimate: every input below is a placeholder assumption, not a measurement.
HORIZON_MONTHS = 36
LOADED_RATE = 120 # assumed loaded cost per engineer-hour
INCIDENT_WEIGHT = 1.5 # an incident hour costs more than an hour of planned toil
# Three-point estimates for one option: (low, likely, high)
inputs = {
"build_cost": (20_000, 40_000, 80_000), # one-time
"toil_hours": (4, 8, 16), # per month
"incident_hours": (5, 10, 20), # per month
"infra_monthly": (1_500, 2_000, 3_000), # per month
}
def tco(build_cost, toil_hours, incident_hours, infra_monthly):
labor = (toil_hours + incident_hours * INCIDENT_WEIGHT) * LOADED_RATE
return build_cost + HORIZON_MONTHS * (labor + infra_monthly)
likely = {name: v[1] for name, v in inputs.items()}
print(f"likely TCO over {HORIZON_MONTHS} months: {tco(**likely):,.0f}")
# One-at-a-time sensitivity: move each input from low to high,
# hold the rest at likely, and record how far the total moves.
swings = []
for name, (low, _, high) in inputs.items():
lo = tco(**{**likely, name: low})
hi = tco(**{**likely, name: high})
swings.append((hi - lo, name, lo, hi))
# The largest spreads are the assumptions reviewers should check first.
for spread, name, lo, hi in sorted(swings, reverse=True):
print(f"{name:15} {lo:>10,.0f} .. {hi:>10,.0f} spread {spread:>10,.0f}")
A short Python script that works out a weighted three-year TCO from low, likely and high inputs, then ranks the inputs by how much each one moves the total.
Worked example: managed message queue vs self-hosted cluster
Illustrative estimate. Every input below is assumed for the example. Substitute your own prices and hours.
Assumed inputs:
- Horizon: 24 months. Loaded rate: $100/hour.
- Managed queue: $0.50 per million messages, plus 1–3 hours/month of integration and quota work.
- Self-hosted cluster: three nodes at $150/month each ($450 total). On-call and upgrades take 6, 12, or 24 hours/month for low, likely, and high.
- Traffic: 300M, 500M, or 800M messages/month.
| Option | Low (24 mo) | Likely (24 mo) | High (24 mo) |
|---|---|---|---|
| Managed | $6,000 | $10,800 | $16,800 |
| Self-hosted | $25,200 | $39,600 | $68,400 |
Self-hosting has the lower infrastructure bill. Its people cost is what loses. In the likely case, 12 hours a month comes to $1,200, nearly three times the node cost.
To find the crossover, set the likely managed cost (0.5 × millions + $200) equal to $1,650. The two meet near 2.9 billion messages/month. That figure is optimistic for self-hosting, because a cluster carrying that load would need more nodes and more hours, which pushes the crossover further out.
What the numbers underweight:
- Managed: Your code and runbooks become coupled to one vendor's semantics. The vendor can change prices on its own schedule. You also cannot tune retention, partitioning, or broker settings past the limits it exposes.
- Self-hosted: The 12 hours are usually spread across only two or three people who understand the cluster. If one of them leaves, the high column becomes the likely column.
Estimating reversal cost before you commit
Reversal cost is easiest to estimate before the first consumer ships. After that, every week adds to it. Four factors drive it:
- Data to move: Count messages, retained topics, and dead-letter backlogs. Note any format conversion as well as raw volume.
- Call sites: Count every producer and consumer that imports the client library or depends on broker-specific semantics such as ordering or delivery guarantees.
- Abstraction layer: A thin internal interface makes a later swap cheaper. You pay for it now, though: every new broker feature has to be wrapped before anyone can use it.
- Coupling growth: Teams adopt features unique to the current choice. Each adoption adds call sites that are harder to port.
The base formula:
reversal_hours = (call_sites × hours_per_site) + migration_hours + (abstraction_hours_per_month × months)
Illustrative estimate (assumed numbers, not a benchmark): 40 call sites × 1.5 h = 60 h, plus 30 h of migration, plus 4 h/month × 12 months of wrapper upkeep = 48 h, for 138 h total. If teams add 3 call sites per month, waiting another 12 months adds 36 sites × 1.5 h = 54 h, before counting any new broker-specific features.
Score each factor from 0 to 2:
| Factor | 0 | 1 | 2 |
|---|---|---|---|
| Data to move | None or disposable | Small, same format | Large or needs conversion |
| Call sites | Fewer than 10 | 10–50 | More than 50 |
| Abstraction | Not needed | Thin wrapper | Wrapper constrains features |
| Coupling growth | Stable | Slow | Teams adopt unique features |
A total of 0–3 means the decision is cheap to reverse: decide quickly and revisit later. A total of 5 or more means it is expensive to reverse, so build the full model with ranges and get it reviewed. A score of 4 calls for judgment, guided by which factor scored highest.
Recording the cost model in your decision records
A cost model kept in a separate spreadsheet stops being useful once the decision ships. Put it in the decision record itself. That way the numbers and the reasoning age together, and a future reader can see what you believed at the time as well as what you chose.
Template for the cost section
- Options considered: each option, including "do nothing" if it was viable.
- Assumptions and sources: one line per input, tagged with its origin: a vendor pricing page (with the date checked), past incident data, or a team estimate (with who made it).
- Horizon: the period costed, and why that length.
- Result: low, expected and high totals per option, plus the formula used so others can rerun it.
- Qualitative costs: hiring difficulty, on-call burden, lock-in, and skills the team gains or loses.
- Revisit triggers: concrete conditions, such as sustained traffic above a stated threshold, team size changing by a set amount, or a vendor pricing change.
Tagging sources tells reviewers which numbers to challenge. A published price is checkable. A team estimate of operational hours is where most of the error usually hides, so it deserves more scrutiny.
Guarding against a tuned model
Models tend to drift toward the option someone already wanted. An on-call estimate shrinks, or the horizon stretches until the upfront cost amortizes away. To counter this, have someone who is not advocating any option set the assumptions before the totals are computed. Log every later change with a reason. This reduces motivated tuning but does not eliminate it, because whoever sets assumptions still knows which inputs move the answer.
## Cost model (illustrative estimate, not measured)
**Options considered:** A) managed queue service, B) self-hosted queue, C) do nothing (rejected: current broker is at end of support)
**Assumptions and sources** (set by: platform lead, not advocating A or B; frozen 2026-09-01)
| Input | Value | Source |
|-----------------------|----------------|----------------------------------------|
| A: service fee | $1,200/month | Vendor pricing page, checked 2026-09-01 |
| A: ops hours | 2 / 4 / 8 h/mo | Team estimate (J. Rivera) |
| B: infra | $500/month | Current cloud bill for similar nodes |
| B: ops hours | 12 / 20 / 35 h/mo | Past incident data, last 12 months |
| B: setup | 60 / 80 / 120 h | Team estimate (M. Chen) |
| Loaded hourly rate | $100/h | Finance planning figure |
**Horizon:** 36 months, matching the expected life of the current traffic profile.
**Formula:** total = (monthly_fee + ops_hours * rate) * 36 + setup_hours * rate
**Result** (low / expected / high):
- A: $50,400 / $57,600 / $72,000
- B: $67,200 / $98,000 / $156,000
**Qualitative costs:** B adds on-call load and requires broker expertise we would
have to hire for; A adds lock-in to one vendor's API and limits.
**Revisit triggers:**
- Sustained traffic above 5,000 msg/s for 30 days
- Platform team size changes by 2 or more people
- Vendor changes pricing for A
**Change log** (every edit after the freeze, with a reason):
- 2026-09-15: B ops hours high raised 30 -> 35; reason: Q3 incident added. By: platform lead
A filled-in markdown template for the cost section of a decision record, with sourced assumptions, a rerunnable formula, low/expected/high results, revisit triggers, and a change log for edits after the assumptions were frozen.
Impact on cost and engineering time: validating and recalibrating estimates
outcome they want. The scheduled review is the counterweight, because it checks the model against what actually happened.
The time savings come before the decision is made. A review that argues over shared hours and ranges converges faster than one arguing over words like "simple" or "operationally heavy." The reversal estimate flags choices that are expensive to undo while changing them is still cheap. Expressing operational toil in engineer-weeks makes the trade-off against product work visible to whoever sets priorities.
Scheduled review
At fixed points, such as six and twelve months after adoption, compare the recorded model with reality:
| Item | Recorded in model | Source of actuals |
|---|---|---|
| Infrastructure spend | Monthly range | Billing export |
| Toil and on-call hours | Hours/month range | Pager history, ops tickets |
| Migration effort | Engineer-weeks range | Tracked tickets |
For each line, compute a calibration factor:
factor = actual ÷ midpoint of estimated range
Illustrative estimate (assumed numbers, not a benchmark): the estimated toil was 10–20 hours/month, so the midpoint is 15. The actual toil was 45 hours/month. The factor is 45 ÷ 15 = 3.0. Use that factor to scale or widen the toil ranges in your next model.
Expect toil to be the weakest input. Guessed hours can be wrong by multiples, while bills usually land closer. A figure like "$4,180/year" with no range suggests precision the inputs cannot support, so keep the ranges.
Records go stale and mislead later readers when nobody owns the review triggers, such as a review date, a traffic threshold, or a pricing change. Put a named owner and a review date in the record header.
How OneEnv can help
Disclosure: OneEnv is the product behind this blog.
OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.
Applied to architecture decision cost, this is where it helps:
Saves time
- Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
- One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
- Changing a route's URL or method automatically notifies the services that consume it — no hunting through repos for callers.
Saves cost
- Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
- Less duplicated configuration to maintain: one shared definition inherited by many services.
- Consumers learn about a breaking change at review time rather than from production errors.
- OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.
These are qualitative benefits, not measured results — how much you save depends on how many services share the value.
Want to try it? Start at oneenv.io.