AW Dev Rethought

🕵️ Debugging is like being the detective in a crime movie where you are also the murderer - Filipe Fortes

Architecture Realities: Designing Around Failure Domains


Introduction:

Every system fails. The question that separates well-designed systems from poorly designed ones is not whether failure will occur but how much of the system fails when it does. A system designed without explicit consideration of failure domains is a system where a single failure can propagate arbitrarily — taking down unrelated functionality, affecting all users simultaneously, and turning a contained problem into a systemic outage.

Failure domains are the boundaries within which a failure is contained. When a failure occurs inside a domain, it affects only the components within that domain and does not propagate to components outside it. Designing systems around explicit failure domains is one of the most effective architectural practices for reducing the blast radius of inevitable failures — and it is one of the practices most consistently neglected during the early stages of system design when the focus is on building functionality rather than containing failure.

Understanding how to identify, define, and enforce failure domain boundaries changes how you think about system architecture at every level — from individual services to global infrastructure.


The Blast Radius Problem:

When a failure occurs in a system without explicit failure domain boundaries, the blast radius — the scope of what is affected — is determined by the system's dependency graph rather than by deliberate design. A database that is shared by multiple services fails all of them simultaneously. A message queue that processes events for unrelated workflows affects all of them when it falls behind. A shared authentication service that becomes unavailable makes every service that depends on it inaccessible regardless of whether those services have anything to do with each other.

Large blast radii turn small failures into large incidents. A bug in a low-priority background job that consumes excessive database connections can starve connection pools for critical user-facing services. A misconfigured rate limit that affects one integration can exhaust shared network resources that affect all integrations. The failure is local but its consequences are global because the system has no boundaries to contain it.

Designing for small blast radii means designing boundaries explicitly — deciding in advance what the maximum scope of any single failure should be and building architectural mechanisms that enforce those boundaries.


Availability Zones and Regions Are Infrastructure Failure Domains:

Cloud providers structure their infrastructure around failure domains at the physical level — availability zones that are isolated from each other within a region, and regions that are isolated from each other globally. Hardware failures, power failures, and network failures are designed to be contained within a single availability zone without affecting others.

These infrastructure failure domains only protect systems that are designed to use them correctly. A system that deploys all of its instances in a single availability zone gets no benefit from the isolation that other availability zones provide. A system that uses a database in one availability zone from application servers in another loses the latency benefits of co-location without gaining meaningful isolation.

Designing around infrastructure failure domains means distributing components across availability zones deliberately, ensuring that the failure of any single zone degrades rather than eliminates system functionality, and understanding which managed services provide cross-zone redundancy automatically and which require explicit configuration.


Service Boundaries Define Application Failure Domains:

At the application level, service boundaries are the primary mechanism for defining failure domains. A monolithic application has a single failure domain — any failure that crashes the process affects all functionality. A well-designed service architecture has failure domains that correspond to service boundaries — a failure in one service affects only the functionality that service provides, and other services continue operating independently.

This benefit only materialises when services are genuinely independent. Services that share databases, that make synchronous calls to each other without timeout and circuit breaker protection, or that depend on shared infrastructure components have failure domains that overlap in ways that eliminate the isolation that separate deployments seem to provide.

Genuine service isolation requires explicit architectural decisions — separate data stores, asynchronous communication patterns that decouple service availability, and dependency management that ensures the failure of any downstream service degrades rather than eliminates the functionality of services that depend on it.


Circuit Breakers Enforce Failure Domain Boundaries at Runtime:

Failure domain boundaries that are defined architecturally but not enforced at runtime provide incomplete protection. A service that is designed to be independent of a downstream dependency but that makes synchronous calls to it without protection will be dragged down when that dependency becomes slow or unavailable — even if the architectural intent was isolation.

Circuit breakers enforce failure domain boundaries at runtime by monitoring the health of downstream dependencies and stopping calls to them when they are failing. When a dependency exceeds a configured error rate or latency threshold, the circuit breaker opens — subsequent calls fail immediately rather than waiting for a timeout, protecting the calling service from the cascading latency that would otherwise accumulate.

Circuit breakers convert hard dependencies into soft ones at runtime. A service that would otherwise fail when its dependency fails instead degrades gracefully — returning cached results, serving reduced functionality, or failing fast with a clear error — while the downstream dependency recovers.


Bulkheads Isolate Resource Pools:

The bulkhead pattern — borrowed from ship design, where compartments are sealed so that flooding in one does not sink the vessel — applies to resource pools in software systems. A service that uses a single thread pool, connection pool, or queue for all of its operations has a single resource failure domain. Resource exhaustion caused by one type of operation affects all operations.

Separate resource pools for different categories of work enforce failure domain boundaries at the resource level. A separate thread pool for background jobs ensures that slow background processing cannot exhaust the threads available for user-facing requests. A separate connection pool for read operations ensures that a write-heavy workload cannot starve read queries of database connections.

Bulkheads accept that resource pools will sometimes be exhausted and design for that exhaustion to be contained. When the background job thread pool is exhausted, background jobs queue up or are shed — but user-facing requests continue processing because they draw from a separate pool that background jobs cannot affect.


Testing Failure Domain Boundaries Requires Deliberate Practice:

Failure domain boundaries that have never been tested under real failure conditions provide theoretical isolation that may not hold in practice. Dependencies that were designed to be isolated may have accumulated coupling that was not intended. Circuit breakers that were configured may have thresholds that are set incorrectly for actual traffic patterns. Bulkheads that separate resource pools may have been bypassed by a shared component that was added without considering the isolation implications.

Testing failure domain boundaries requires deliberately inducing failures — taking down dependencies, exhausting resource pools, introducing latency into downstream services — and verifying that failures are contained within their intended domains. This is the practice of chaos engineering applied specifically to failure domain validation rather than general resilience testing.

Teams that test their failure domain boundaries regularly discover boundary violations in controlled conditions rather than during real incidents. The cost of discovering a boundary violation during a chaos engineering exercise is a brief period of degraded functionality in a controlled environment. The cost of discovering it during a production incident is a large blast radius that could have been contained.


Conclusion:

Failure domains are not an advanced architectural concern to be addressed after a system is mature. They are a fundamental design consideration that determines how much of a system fails when any individual component fails — and that question matters from the first day a system serves real users.

Systems designed around explicit failure domains fail smaller, recover faster, and cause less user impact than systems where failure boundaries are determined by dependency graphs rather than deliberate design. The investment in defining those boundaries early, enforcing them architecturally and at runtime, and testing them regularly is one of the most reliable ways to improve system reliability without reducing the pace of feature development.


If this article helped you, you can support my work on AW Dev Rethought.


Rethought Relay:
Link copied!

Enjoyed this post?

Stay in the loop

New posts + weekly digest, straight to your inbox.

or

Create a free account

  • Save posts to your vault
  • Like posts & build history
  • New-post alerts

Comments

Add Your Comment

Comment Added!