DEV Community

Cover image for Good Architecture Plans for Failure
Michael Masterson
Michael Masterson

Posted on • Originally published at m2s2.io

Good Architecture Plans for Failure

Good architecture does not assume everything will work.

It assumes something will fail and makes sure the team has a reasonable way to contain it, understand it, and recover from it.

That may sound pessimistic, but it is really practical engineering. Every production system eventually meets reality. Dependencies slow down. Networks misbehave. Deployments change behavior. Queues back up. Users retry actions. Data arrives in shapes no one modeled perfectly.

Failure is not always a sign that the architecture is bad.

Failure is part of the environment the architecture has to operate within.

In the first article of this series, I wrote about how good architecture creates engineering momentum by making important decisions visible early. In the second, I wrote about observability because teams need to understand and reason about systems in production.

Failure planning is the next step.

Observability helps a team see what is happening. Failure planning gives the team options when something goes wrong.

A system is not well-designed because nothing ever fails. A system is well-designed when expected failures are contained, visible, and recoverable enough that they do not create unnecessary chaos.

The Happy Path Is Not the Whole Design

Most systems start with the happy path.

A user takes an action. The API receives a request. The service validates the input. The database saves the record. An event is published. A downstream process reacts. The workflow completes.

That path matters. It is usually the reason the system exists in the first place.

But production systems are shaped just as much by the paths that do not go cleanly.

The database write succeeds, but the event publish fails. A third-party service times out after the user has already submitted the request. A message is processed twice. A worker crashes halfway through a job. A deployment changes behavior in one service but not another. A user refreshes, retries, or submits the same action again because the interface did not clearly tell them what happened.

These are not strange edge cases. They are normal conditions in real systems.

When the design only accounts for the successful flow, the team is forced to discover the failure flow later. Usually that happens under pressure, when production is already misbehaving and the cost of learning is higher.

Good architecture does not require every failure path to be solved upfront. That would be unrealistic. But it should make the most important failure paths visible early enough for the team to decide how they want the system to behave.

That is the difference between accidental behavior and intentional design.

Reliability Lives in the Details

Some of the most important architecture decisions look small at first.

Timeouts are a good example. A timeout is not just a configuration value. It defines how long one part of the system is allowed to wait on another before protecting itself. Without clear timeouts, a slow dependency can hold resources, block requests, and create pressure in places that were never the original source of the problem.

Retries are similar. Retrying a request can make a system more resilient when the failure is temporary. It can also make an outage worse if every client retries aggressively at the same time. A retry can recover work, or it can duplicate work. It depends on how the system is designed.

That is where idempotency matters.

If an operation may be attempted more than once, the system needs to know whether repeating it is safe. This matters for APIs, payments, event processing, background jobs, notifications, provisioning workflows, and any system where partial success is possible.

A retry without idempotency is not resilience. It is a bet that the same action will not hurt you twice.

These decisions often get treated as implementation details, but they shape the behavior of the system. They determine whether a failure stays small or spreads. They determine whether recovery is clean or messy. They determine whether the team can confidently automate retry behavior or has to manually clean up after every partial failure.

Good architecture takes these details seriously because reliability is rarely created by one big decision. It is usually created by many small decisions that work together.

Contain the Blast Radius

Once something fails, the next question is how far the failure should spread.

That is where blast radius matters.

Not every part of a system has the same business importance. If a reporting workflow is delayed, that may be acceptable. If checkout, login, or a core customer workflow fails, the impact is much higher. Good architecture recognizes that difference and avoids letting non-critical failure take down critical behavior.

This is where boundaries matter. Service boundaries, queue boundaries, rate limits, circuit breakers, fallback behavior, and feature flags are all ways to control how failure moves through a system. None of them are magic. Each adds its own complexity. But when used intentionally, they give the team more control over how the system behaves under pressure.

A downstream service being unavailable should not automatically mean every upstream workflow has to fail. A slow dependency should not be allowed to consume every resource in the system. A bad release should not require the entire platform to be rolled back if the change can be isolated or disabled.

The point is not to make every system bulletproof.

The point is to decide which failures are acceptable, which failures must be contained, and which parts of the system need to degrade gracefully instead of collapsing completely.

That is an architectural decision.

It is also a business decision.

Recovery Is Part of the Design

Prevention gets a lot of attention, but recovery matters just as much.

No matter how carefully a system is designed, something will eventually go wrong. A deployment will introduce an issue. A migration will behave differently than expected. A message will fail. A third-party service will return something unusual. A job will process only part of the data it was supposed to process.

The question is not only whether the team can prevent every problem.

The question is whether the team knows what to do next.

A system that supports recovery gives the team options. Failed messages can be retried or moved to a dead-letter queue. Events can be replayed. Data migrations have a rollback or repair strategy. Deployments can be reverted safely. Runbooks explain what to check and who owns the response.

These things do not need to be overbuilt, but they should not be ignored.

A system becomes more trustworthy when the team knows how to recover it. Engineers move differently when they know a failed job can be replayed, a bad deploy can be rolled back, or a partial failure can be repaired without guessing through production data.

Recovery creates confidence.

Confidence creates momentum.

Preparation Is Different Than Planning

Planning for failure is important, but planning is only half the work.

The other half is preparation.

A team can document a rollback process, but that does not mean everyone knows how to use it under pressure. A system can have a recovery path, but that does not mean the path has been tested recently. A runbook can describe what to do during an outage, but if no one has practiced the steps, the first real incident becomes the training session.

That is not where teams want to learn.

Good architecture gives teams options when something goes wrong. Good preparation makes sure the team knows how to use those options.

This is where practices like chaos days, game days, failure drills, and incident simulations become valuable. The goal is not to randomly break things for entertainment. The goal is to safely test assumptions, expose weak spots, and build confidence before the stakes are higher.

A team might simulate a dependency outage, force a queue to back up, test a rollback, replay failed messages, rotate a secret, restore from backup, or walk through what happens when a critical service becomes unavailable. These exercises do not need to be large or dramatic to be useful. Even a small controlled failure can teach the team a lot about the system.

The value is often in what the team discovers.

Maybe an alert fires too late. Maybe the runbook is missing a step. Maybe only one person knows how to perform the recovery. Maybe a fallback path exists in code but has never been used in production. Maybe the system technically recovers, but the customer experience is worse than expected.

Those are useful discoveries.

Finding them during a practice exercise is far better than finding them during a real incident.

Preparing for failure also changes team behavior. Engineers become more familiar with operational tools. Leaders gain a clearer understanding of system risk. Teams learn where the architecture is resilient and where it is still fragile.

That kind of practice builds confidence.

Planning gives the team a theory of recovery. Preparation proves whether that theory holds up.

Failure Planning Depends on Observability

Failure planning, preparation, and observability are closely connected.

Observability helps the team see failure. Planning gives the team recovery options. Preparation teaches the team how to use those options when the system is under pressure.

One without the others is incomplete.

If a system has strong failure handling but weak observability, the team may not know when something is degrading or why. If a system has strong observability but no recovery path, the team may know exactly what went wrong and still have no safe way to fix it. If the team has a recovery plan but has never practiced it, the plan may not survive first contact with a real incident.

The best systems give teams visibility, options, and rehearsal. They show where the failure happened, how far the impact spread, and what recovery paths are available. They also give the team enough practice to move from detection to understanding to action without starting from scratch.

This is why failure planning belongs in the architecture conversation, not just the incident response process.

By the time a system is failing in production, the team is mostly working with the options the architecture already gave them and the preparation they already invested in.

Good architecture gives them better options.

Good preparation helps them use those options well.

Failure Design Is a Business Decision

Not every system needs the same level of resilience.

A prototype does not need the same failure planning as a payment system. An internal admin tool does not need the same guarantees as a customer-facing workflow. A reporting job that can run late does not need the same recovery model as a real-time user action.

That is why designing for failure is not about making everything equally resilient. It is about matching the investment to the impact.

Resilience has a cost. Redundancy, queues, retries, fallback paths, data replication, disaster recovery, and operational tooling all add complexity. Sometimes that complexity is justified. Sometimes it is not.

Good architecture is honest about those tradeoffs.

It considers what the business needs, what the team can operate, and what the system is responsible for protecting. It separates critical workflows from secondary ones. It identifies where failure must be avoided, where it can be tolerated, and where graceful degradation is good enough.

The goal is not perfection.

The goal is intentionality.

When the team understands the consequences of failure, they can make better decisions about where to invest in reliability and where to keep the system simple.

From Failure Paths to Forward Momentum

Good architecture does not eliminate failure. It gives teams a better way to handle it.

That matters because engineering momentum is not just about how quickly a team can ship when everything is going well. It is also about how quickly the team can recover when something goes wrong.

A team that understands its failure paths can move with more confidence. A team that has clear recovery options can release with less fear. A team that practices its response is less likely to discover basic operational gaps during a real incident.

Failure planning is not about pessimism. It is about responsibility.

It is about recognizing that production systems operate in imperfect environments and designing them so that failure does not automatically become chaos.

That is why good architecture plans for failure.

It helps teams contain the impact, understand what happened, recover with confidence, and keep moving.

MARC², the M²S² architecture review bot, can help start that conversation by surfacing areas where failure behavior, reliability assumptions, or recovery paths may need more attention. From there, M²S² can help your team reason through the tradeoffs and turn those insights into practical architecture decisions.

MARC² can help you start the conversation. M²S² can help you turn it into action.

Top comments (0)