July 8, 2026
Why Most Software Outages Aren’t Bugs — They’re Changes
Software Development

By JIN
13 min read
Disclosure: I use GPT search to collection facts. The entire article is drafted by me.
The real risk in software was never the code itself. It's the moment the code starts changing.
Software engineering has spent the last two decades chasing one goal: ship faster. Monoliths gave way to microservices, CI gave way to CD, DevOps gave way to GitOps — and the industry kept shrinking the distance between "commit" and "production." Many companies now deploy dozens or hundreds of times a day, and large cloud platforms routinely push thousands of production changes daily.
That speed created real competitive advantage. It also created a problem nobody fully priced in.
Are systems actually getting more stable? Not necessarily.
A lot of teams notice something that feels contradictory: test coverage keeps improving, code review gets stricter, monitoring gets richer — and yet production incidents don't drop the way you'd expect. What's changed is what causes those incidents. Fewer of them look like "the code was written wrong." More of them happen after something in the system moved — a config edit, a database field tweak, a dependency bump, a "harmless" interface refactor — and that movement propagated silently through a call chain until it became a business-wide outage.
The industry is slowly arriving at an uncomfortable realization: the bug was never the risk. The change is where the risk actually begins.
1. Modern software changes constantly — that's the baseline now
A decade or so ago, most enterprise software still ran on monolithic architectures. A system might ship a handful of releases a year, on a fixed deployment window, with every module going out together. "Going live" was closer to a project milestone than a daily engineering action.
That world is gone. DoorDash's delivery platform, for example, has evolved into a distributed system made up of hundreds of microservices — orders, payments, dispatch, maps, merchants, coupons, and notifications each running as independent services with independent release cadences. For a platform like that, "shipping" isn't a special event anymore. It's a continuous background activity.
On an ordinary weekday, production may absorb hundreds — or more — distinct changes. And those changes aren't limited to application code. They include new feature rollouts, service config adjustments, Kubernetes cluster upgrades, database schema migrations, RPC/HTTP interface changes, third-party SDK updates, cache policy tweaks, gray-release rule adjustments, cloud infrastructure upgrades, and even AI model swaps.
In other words: a modern software system is essentially never in a stable, unchanging state. Constant change drives fast business iteration — and constant change is also a constant source of new uncertainty.
2. The biggest risk in software isn't "wrong code"
Most people's first mental model of software quality treats risk and bugs as the same thing. They're not.
A bug is an outcome. Risk is closer to a probability — a condition that could produce an outcome, but hasn't yet.
Take a simple example. A developer modifies the amount-calculation logic inside a payment service. The code has no syntax errors. Unit tests pass. Automated tests pass. Code review goes smoothly. From a process standpoint, this change looks clean.
Then it ships — and a batch of abnormal orders appears. The postmortem finds the bug only triggers under one rare coupon combination, a real-world scenario the test environment never modeled. Some orders end up with the wrong charged amount.
The actual cause here wasn't "the code was wrong." It was that a perfectly normal-looking change introduced a new risk the tests were never designed to see. This pattern is common in large internet-scale systems: the incident isn't caused by the program failing to run — it's caused by the program continuing to run, just in a way nobody anticipated. That kind of risk is much harder to catch than a syntax error, because everything that can be checked, checks out fine.
3. Why one small change turns into a systemic incident
Modern software's defining trait is tight coupling — and not just at the code level. The deeper coupling is between business processes.
An order-creation endpoint, for instance, typically touches the user service, inventory service, payment service, coupon service, risk engine, recommendation system, message queue, search index, and data warehouse simultaneously. A developer might only touch one of those services directly. What actually gets affected is the entire call chain downstream of it.
That's why large internet companies increasingly talk about "blast radius" instead of "diff size." The relevant question stops being what did I change and becomes what does this change ultimately reach. Those are structurally different questions, and answering the second one requires infrastructure the first one never needed.
4. Most major incidents are, at their core, change-management failures
Looking back at some widely discussed internet outages, a pattern repeats: the root cause is rarely hardware failure. It's almost always a routine engineering change.
Knight Capital's 2012 trading incident is the textbook case in this space — a new deployment reactivated a long-dormant piece of legacy code, generating a flood of erroneous trades within 45 minutes and producing losses that reportedly exceeded $400 million. Cloudflare has experienced a global service disruption traced back to a single misconfigured rule — not a server failure, not an attack, but a normal-looking configuration change triggering an unanticipated chain reaction in production. Similar dynamics have shown up at GitHub, GitLab, and AWS. The common thread: the system was working fine right up until it started changing.
To be fair, treating every outage as a "change risk" problem is an oversimplification — natural disasters, regional network failures, aging hardware, and third-party outages can all take a system down too, and reliability is genuinely multi-dimensional. But across a growing body of public incident postmortems, engineering change keeps surfacing as one of the most frequent and most preventable risk sources — which is exactly why Change Risk Management has become its own discipline. The questions worth asking have shifted from "is there a bug?" to: which changes carry the highest risk, which interfaces get touched, which dependencies might break, which tests need to rerun, which services need a gradual rollout, and where should alerts fire early.
Software quality management is quietly shifting from bug-hunting to risk management.
5. The hard part was never writing the code
Most engineers will recognize this pattern: writing the code takes an afternoon. Everything after that takes the rest of the week.
Which other services does this endpoint affect? Did I touch a shared DTO? Did I break backward compatibility? Does the cache need updating? Will the database index still get hit? Do I need new monitoring metrics? Which user segment should the gradual rollout target? Is there a rollback plan?
None of those questions are about coding. They belong to a bigger discipline: software change management. Code is just where the change physically happens. What actually determines the size of the risk is the path that change propagates along through the rest of the system — and whether the team can see that path before shipping.
Seen this way, the first real question a mature quality system needs to answer isn't "is the code broken?" It's: what did this change actually alter? Only once a team can answer that does testing strategy, regression automation, gradual rollout, compatibility checking, and risk scoring have a shared foundation to work from. That's the actual reason large companies keep building internal code-change-analysis platforms.
6. Why traditional testing struggles to answer "what did this actually affect?"
Most teams already run a fairly mature pipeline: lint checks, unit tests, automated tests, code review, CI/CD, gradual rollout, production. These tools have genuinely reduced low-level errors over the last decade.
So why do incidents still happen after every stage passes? Usually it's not that the tools failed — it's that the question they answer has quietly changed underneath them.
Tests verify function, not blast radius. A developer modifies inventory-deduction logic in an order service — a routine feature change. Unit tests pass, integration tests pass, review finds nothing, staging looks fine. Hours after release: coupons stop working, merchant dashboards show wrong inventory, and recommendation rankings start fluctuating. The root cause: the change altered when inventory state updates, and multiple downstream systems depended on that timing. The developer touched inventory. The blast radius covered the entire business chain. Tests answer "does this function behave as expected" — not "what else does this quietly touch," and those are different questions with different tooling requirements.
Code review increasingly depends on the reviewer's personal experience. Two commits can look identical in diff size — renaming a field in UserService versus modifying order-state transition logic in PaymentService — but carry wildly different risk. The second change may ripple into payment callbacks, reconciliation, risk control, marketing campaigns, notifications, and financial settlement. If the reviewer doesn't already carry that dependency map in their head, a 40-line diff won't reveal it. Most of a senior engineer's review time isn't spent reading the diff — it's spent mentally reconstructing system context: who calls this, what does it call, is there a public interface, a message being sent, a config dependency, a schema change, a backward-compatibility risk. Most code hosting platforms still only show text changes, not system changes — which is exactly the gap.
Git tells you what changed. It never tells you what that change reaches. Git can tell you precisely which file, which line, who committed it, and when. That's sufficient for collaboration. It's nowhere near sufficient for risk analysis. If a method signature changes, Git has no way to answer how many callers depend on it, which RPC services rely on it, whether async consumers are involved, or whether it touches a database transaction. That information sits in a different layer entirely — code relationships, not code diffs — which is exactly why companies increasingly separate "diff" from "impact" as two distinct analytical problems.
In microservices, the hardest part isn't the code — it's the dependency graph. A single "place order" action might touch dozens or hundreds of services. DoorDash has publicly discussed how, as service count grows, engineers increasingly can't hold the full dependency graph in their heads — which is why they've invested in service catalogs, dependency analysis, and release-risk tooling to lower the cognitive cost of understanding the system. At scale, the expensive activity stops being writing code and becomes understanding it.
This is exactly why "Blast Radius" has become a recurring term inside engineering orgs at Google, Netflix, Uber, and DoorDash — borrowed from explosives terminology, now describing how far a single change can reach through a live system. A field added to a shared DTO looks trivial in a diff. Whether it's actually safe depends on whether Android can still parse it, whether iOS depends on field ordering, whether a third-party integration already consumes the old schema, whether a Kafka consumer stays compatible. None of that shows up by reading the change itself.
7. From Git diff to Change Graph: how these systems get built
Almost every code-analysis system starts the same way — parsing a Git diff. That tells you which lines changed. It says nothing about propagation.
The first structural step is teaching the system to actually parse code into something analyzable, typically via an Abstract Syntax Tree (AST). A line like price = amount - discount; becomes a structured tree — an assignment, containing a subtraction expression, containing two variables. That's already more information than a raw diff: it identifies the operation as a business calculation, not just changed text.
AST alone hits limits fast, though. Java lambdas compile down to multiple underlying method calls the source doesn't show directly. Generics get erased at compile time. Dynamic proxies — Spring AOP, CGLIB, JDK proxies — create call relationships that simply don't exist anywhere in the source file. Relying purely on AST produces an incomplete call graph.
This is why many large Java platforms add bytecode analysis (commonly via ASM) on top of AST — because compiled bytecode retains execution information the source doesn't: real method call targets, compiler-generated bridge methods, expanded lambda forms, and which implementation an interface call actually resolves to at runtime. Even bytecode has blind spots — reflection, dynamic classloading, SPI plugin mechanisms — which only manifest once the program actually runs. That gap is why some teams layer in runtime trace data (production call chains, APM, distributed tracing) to correct the static graph after the fact — a static-plus-dynamic approach that's gaining traction, at real cost in complexity and maintenance.
// What Git sees: one line changed
- reserve(String sku)
+ reserve(String sku, int quantity)
// What a call-graph engine needs to answer instead:
// 1. Which controllers call InventoryService.reserve()?
// 2. Which RPC clients depend on this signature?
// 3. Which Kafka consumers eventually trigger this path?
// 4. Which scheduled jobs route through here?
// 5. Which DB transactions span this call?// What Git sees: one line changed
- reserve(String sku)
+ reserve(String sku, int quantity)
// What a call-graph engine needs to answer instead:
// 1. Which controllers call InventoryService.reserve()?
// 2. Which RPC clients depend on this signature?
// 3. Which Kafka consumers eventually trigger this path?
// 4. Which scheduled jobs route through here?
// 5. Which DB transactions span this call?This snippet illustrates the actual gap between diff-level and graph-level analysis: the code change is trivial to describe, but answering "who is affected" requires a fully materialized call graph, not a diff parser. Once enough of these relationships — method calls, RPC calls, HTTP calls, message publishing, scheduled jobs, config reads, DB access — get stitched together, you get a real Call Graph, which becomes the foundation every downstream capability (impact analysis, automated test recommendation, compatibility checking, config risk detection) is built on top of.
Storing that graph is its own problem. Relational databases handle a handful of call relationships fine; they buckle once you're asking "find every downstream caller of this method" or "find every path that eventually reaches the payment service" across tens of millions of edges — because that's graph traversal, not row lookup. That's the practical reason many platforms migrate this data into graph databases: not because graphs are fashionable, but because the object being queried has genuinely changed from data to relationships.
8. The harder problem: turning a graph into a risk judgment
Even a complete call graph doesn't tell you what's actually dangerous. Not all changes carry equal weight, and treating them as equivalent defeats the purpose.
Adding a log statement and adding @Transactional might both show up as three-line diffs. The first almost never changes business behavior. The second can change lock contention, rollback semantics, data consistency, call-chain latency, and database load. A system that can't distinguish these treats every change identically — which isn't useful.
Early attempts at risk scoring (add 5 points for a DB change, 3 for an RPC change, sum it up) run into a wall fast, because risk isn't an inherent property of code — it's contextual. A database write to a user nickname field and a database write to a payment ledger are technically the same kind of operation and carry entirely different risk. Whether it's an experimental feature or the core order state machine changes everything. So the more durable approach isn't scoring — it's pattern recognition: transaction patterns, cache patterns, messaging patterns, config patterns, compatibility patterns, permission patterns, funds-handling patterns, pagination, retry, rate-limiting, degradation. Each annotation in code is effectively a risk signal the developer left behind without necessarily realizing it — @Async signals a sync-to-async transition, @Cacheable signals new cache-consistency concerns, @Scheduled introduces a time dimension, @KafkaListener opens the door to duplicate consumption and eventual-consistency issues, a new FeignClient call introduces a fresh cross-service dependency with its own timeout/retry/circuit-breaker considerations.
But technical signals alone aren't enough — the deepest risk usually comes from business semantics, not implementation. Two endpoints — /updateUserAvatar and /createSettlementBill — can be technically identical: same controller/service/repository/cache/queue pattern. A code-only analyzer sees no difference. The business consequence of failure is wildly different: a broken avatar upload means "upload again," a broken settlement bill can mean incorrect payouts, duplicate transfers, financial exposure, and regulatory risk. This is why mature risk systems eventually accumulate business tags — Payment, Settlement, Coupon, Inventory, Permission, Compliance, PII, GDPR — which is business knowledge, not code analysis.
Because no central platform team can realistically maintain risk rules for every domain (payments cares about amounts, idempotency, transactions; coupons cares about stacking rules and inventory; recommendation cares about ranking and feature consistency; risk control cares about rules and models), a common structural pattern emerges: the platform owns the engine, individual business teams own the knowledge, contributing their own risk rules the way teams contribute plugins to a shared IDE. That division of labor scales better than a central team trying to encode hundreds of domain-specific rules on its own.
This is also, functionally, how organizations turn painful incidents into permanent institutional memory. A payment team's default-config incident becomes a new rule checking all config defaults. An order team's duplicate-message incident becomes a Kafka idempotency check. An open-platform DTO-deletion incident becomes a public-interface compatibility check. Over time, the platform starts resembling a living incident encyclopedia — every past outage becoming a rule that fires before the next one repeats.
9. Does AI change any of this?
Large language models have gotten genuinely good at reading code across files and modules, and plenty of teams are experimenting with feeding a Git diff to a model and asking which modules are affected, which tests might be missing, or whether a change touches fund-handling logic. In cross-file, cross-repo context building, this has real value.
But it's premature to expect LLMs to replace change-risk platforms outright, for three concrete reasons. First, models understand language; platforms encode rules — and a lot of that rule knowledge (which interfaces require gradual rollout, which payment services can't ship directly, which databases can only change at 3am) was never written down anywhere a model could read it; it's tribal organizational knowledge. Second, risk decisions need certainty, and models produce probability — "this might have a compatibility issue" isn't the same thing a release-approval process needs, which is a yes/no. Third, engineering gates need to be auditable — "why was this blocked" needs a traceable reason, not "the model thought so."
The more realistic near-term shape isn't Rule vs. AI, it's Rule plus AI: rules handle deterministic checks, AI handles context synthesis, test suggestions, risk explanation, and report generation. That combination is more practically useful right now than betting entirely on either approach.
10. My take: the goal was never zero incidents — it's shrinking the unknown
I don't think any change-risk platform, no matter how sophisticated, will ever eliminate production incidents, and I'd be skeptical of anyone claiming otherwise. Code isn't the whole system — configuration, databases, caches, message queues, containers, networking, DNS, service discovery, third-party APIs, and raw user traffic (a Super Bowl surge, a Black Friday spike) all sit outside version control entirely, and no static analyzer will ever see a holiday traffic spike coming from a diff.
The realistic goal is narrower and more honest: shrink the size of the unknown before every release, not eliminate it. That means combining static analysis, runtime trace data, monitoring, tracing, logs, and production traffic into one continuous risk picture — because static analysis alone, however sophisticated, only ever answers the code-level half of the question.
Where I'd push back on the more AI-optimistic crowd: the momentum here is genuinely toward Rule+AI hybrids, not AI-as-decision-maker, and for good reason — audit trails and organizational tribal knowledge aren't things a language model can currently be trusted to hold reliably on its own.
11. What this actually changes in practice
Once a team internalizes this, the workflow changes in specific, visible ways: technical design docs get automatically diffed against actual code changes to catch drift; code review interfaces surface upstream/downstream call relationships directly instead of forcing reviewers to reconstruct context by hand; "blast radius" replaces "lines changed" as the metric that determines review depth; configuration changes get treated with the same rigor as code changes, since a huge share of real incidents trace back to config, not logic; API/DTO compatibility gets checked as its own risk category, independent of functional correctness; automated test selection gets driven by what a change actually touches rather than which module it nominally belongs to; and institutional near-misses get systematically converted into standing rules instead of staying folklore held by a few senior engineers.
Conclusion
Software engineering is quietly shifting its center of gravity from "did we write a bug" to "did this change introduce something we didn't anticipate." A bug is an outcome; a change is the cause — and treating them as the same problem is exactly why teams with excellent test coverage still get surprised in production. The practical, memorable version of this shift: stop asking only whether the code is correct, and start asking what, specifically, this change altered — which interfaces it touches, which dependencies shifted, which compatibility assumptions it breaks, and how far that ripple actually travels. Change-risk analysis won't get you to zero incidents. It gets you to every release being made with more visibility into its own consequences than the one before it — and in a system that never stops changing, that's the only kind of stability actually available.
If you'd like to show your appreciation, you can support me through:
✨ Patreon ✨ Ko-fi ✨ BuyMeACoffee
Every contribution, big or small, fuels my creativity and means the world to me. Thank you for being a part of this journey!