Skip to main content
Technical Systems

Why Most Systems Are Over-Abstracted and Under-Isolated

Seven layers deep and one failure still takes down everything

Systems pile on abstraction layers that hide behavior while sharing failure domains across components. Over-abstraction blocks debugging; missing isolation propagates cascading failures.

Why Most Systems Are Over-Abstracted and Under-Isolated

Software architecture is often described as a way of managing complexity.

I don’t think that’s entirely true.

Complexity rarely disappears.

Architecture simply decides who gets to see it.

Every abstraction hides implementation.

Every interface hides behaviour.

Every framework hides execution.

Every shared dependency hides coupling.

Those decisions make software easier to write because developers no longer need to understand every implementation detail before they can build something useful.

The difficulty is that production failures rarely respect those boundaries, which is why some systems cannot be tested without production.

The information hidden during development often becomes the information engineers desperately need during an incident.

Good architecture isn’t about hiding as much complexity as possible.

It’s about deciding which complexity should remain visible.

Every Architecture Creates Blind Spots

Every architectural decision removes information from someone’s view.

That’s usually intentional.

A database driver hides network protocols.

An ORM hides SQL.

A service layer hides persistence.

A messaging library hides retries, acknowledgements and connection management.

Without abstraction, every application would become unmanageable.

The goal isn’t to eliminate abstraction.

The goal is ensuring the abstraction hides implementation details rather than operational behaviour.

Those are very different things.

Implementation details answer questions such as:

  • Which protocol is being used?
  • Which database driver is installed?
  • Which serialisation format was chosen?

Operational behaviour answers different questions:

  • What happens if this dependency fails?
  • How long will retries continue?
  • Which resources are shared?
  • Where does execution stop?

Those answers become critically important during production incidents.

If the abstraction hides them, the engineer investigating the failure is forced to understand the abstraction itself before they can understand the system.

Hiding the Wrong Things

             Application


             Service Layer


                  ORM


             Database Driver


                PostgreSQL

Implementation hidden ✓

──────────────────────────────

Retry policy

Connection pooling

Timeout behaviour

Shared resources

Failure propagation

Hidden ✗

The abstraction succeeded technically.

It failed operationally.

Abstraction Should Hide Mechanics, Not Behaviour

One reason abstraction becomes problematic is that behaviour gradually disappears behind increasingly convenient interfaces.

Consider a typical application service.

User Service


Repository


ORM


Connection Pool


Database

From the perspective of the service, retrieving a customer appears to be a single operation.

Internally, far more is happening.

A connection is acquired.

Retries may occur.

Transactions may begin.

Locks may be obtained.

Queries may be cached.

Timeouts may be enforced.

Permissions may be evaluated.

None of these are implementation details once the system enters production.

They’re observable behaviours.

When performance degrades or failures occur, those behaviours determine how the system responds.

An abstraction that completely hides them may make development easier while making diagnosis considerably harder.

The engineer no longer needs to understand how the code works.

They need to understand how the abstraction works.

Isolation Solves a Different Problem

Abstraction is often discussed alongside isolation because both introduce boundaries.

They solve different problems.

Abstraction exists to reduce cognitive load.

Isolation exists to reduce failure propagation.

One makes systems easier to understand.

The other makes systems easier to recover.

These goals occasionally reinforce one another.

Just as often they conflict.

A beautifully abstracted system can still allow every component to depend on the same database, cache and authentication service.

Likewise, a highly isolated system may expose many more operational details because understanding those boundaries is necessary for diagnosing failures.

The architectural challenge isn’t choosing abstraction or isolation.

It’s deciding what each boundary is supposed to achieve.

Two Different Boundaries

          Abstraction

 Developer


   Interface


Implementation

Hides complexity.


──────────────────────────────


           Isolation

 Service A


 Failure Boundary


 Service B

Contains complexity.

One boundary simplifies development.

The other limits operational impact.

Confusing the two often produces systems that are simultaneously difficult to understand and difficult to recover.

Every Hidden Dependency Becomes a Future Surprise

One of the recurring themes in production systems is that dependencies rarely disappear.

They simply become less obvious.

A dependency injection framework resolves shared services automatically.

An infrastructure library creates a shared connection pool.

A framework registers middleware behind configuration.

A service mesh transparently retries failed requests.

The code appears remarkably simple.

The operational behaviour has become significantly more complicated.

None of these technologies are inherently problematic.

The problem appears when the architecture no longer makes important dependencies visible.

A shared cache is still shared whether or not the application code references it directly.

A retry policy still exists whether or not it’s configured through annotations instead of explicit code.

The dependency hasn’t been removed.

Only the visibility has changed.

That’s an important distinction because failures don’t care whether the dependency was obvious during development.

They only care that it existed.

Shared Resources Create Hidden Coupling

One of the more subtle consequences of abstraction is that it often hides where systems actually depend on one another.

Two services appear independent.

They have separate repositories.

Separate deployment pipelines.

Separate APIs.

Separate teams.

From the source code alone, they look isolated.

Operationally, they may be sharing the same database.

The same cache.

The same message broker.

The same connection pool.

The same authentication service.

The dependency hasn’t disappeared.

It has simply moved somewhere the application code no longer reveals.

That distinction becomes important during failures.

When a shared dependency becomes unavailable, every system relying on it begins failing simultaneously.

From the perspective of each team, the failure appears unrelated.

From the perspective of the architecture, the systems were never independent in the first place.

Hidden Coupling

          Service A





         Repository


────────────────────────────


          Service B





         Repository


────────────────────────────


        Hidden Dependency





      Shared Connection Pool





          PostgreSQL

The services appear independent.

The failure domain is shared.

One of the recurring lessons in production systems is that architectural independence and operational independence are not the same thing.

Isolation Exists to Limit Blast Radius

Isolation is often described as a reliability feature.

That’s true.

It’s also a debugging feature.

When failures remain contained, engineers can reason about them locally.

If an image processing service fails but payment processing continues normally, the investigation begins with the image processing service.

The failure boundary is obvious.

Now consider the opposite.

An image processing workload exhausts a shared thread pool.

Authentication requests begin timing out.

API latency increases.

Background jobs stop executing.

Database connections become unavailable.

The original failure still occurred inside image processing.

Everything else became involved because the systems shared resources.

The investigation has become dramatically more difficult.

Not because the failure itself is more complicated.

Because the architecture no longer tells engineers where the problem should be.

Failure Containment

           Isolated Services

Image Service      Payment Service

      │                  │

      ▼                  ▼

 Resource A        Resource B

      │                  │

 Failure



Only Image Service Fails


────────────────────────────────


         Shared Resources

Image Service      Payment Service

      │                  │

      └────────┬─────────┘



        Shared Resource



            Failure



Both Services Fail

Isolation doesn’t prevent failures, and missing isolation is one reason retry policies amplify outages.

It prevents failures becoming everyone else’s problem.

Frameworks Optimise the Happy Path

Modern frameworks are remarkably effective at reducing repetitive work.

Routing.

Dependency injection.

Authentication.

Configuration.

Caching.

Retries.

Metrics.

Logging.

All of these concerns can be introduced with remarkably little application code.

This is a genuine improvement.

The challenge is that frameworks primarily optimise development.

Production failures optimise for something else entirely.

Understanding behaviour.

Suppose a request fails because authentication repeatedly retries a remote identity provider.

The retry logic isn’t visible in the application.

The timeout isn’t visible.

The circuit breaker isn’t visible.

The fallback policy isn’t visible.

Those decisions exist somewhere inside the framework.

The application appears simple precisely because somebody else now owns its operational behaviour.

The Happy Path

        HTTP Request





      Framework Magic

Authentication

Retries

Caching

Authorisation

Metrics

Logging





       Controller Logic

Development sees one function.

Production executes many.

Frameworks don’t remove complexity.

They relocate it.

Whether that’s beneficial depends entirely on whether the hidden behaviour still matters when diagnosing production incidents.

Every Layer Becomes Another Place to Fail

One of the reasons layered architectures become increasingly difficult to operate is that every abstraction introduces another execution boundary.

A request may pass through:

  • a reverse proxy
  • an API gateway
  • middleware
  • dependency injection
  • application services
  • repositories
  • ORMs
  • connection pools
  • database drivers
  • the database itself

Every layer performs useful work.

Authentication.

Validation.

Transformation.

Caching.

Logging.

Retries.

Metrics.

Transactions.

Each also introduces another place where latency may increase.

Configuration may differ.

Resources may become exhausted.

Timeouts may occur.

The architecture becomes deeper.

The failure surface becomes larger.

Layered Execution

          Client


       API Gateway


        Middleware


      Application Service


        Repository


             ORM


     Connection Pool


         Database

Every boundary is another opportunity
for useful behaviour.

Every boundary is another opportunity
for unexpected behaviour.

This doesn’t mean layered architectures are inherently flawed.

It means every additional layer should justify the operational complexity it introduces.

Local Simplicity Can Create Global Complexity

One reason these architectures emerge so naturally is that every individual decision appears reasonable.

A repository avoids duplicated queries.

Dependency injection improves testing.

Middleware centralises authentication.

Caching improves performance.

Retries improve resilience.

None of these decisions are obviously wrong.

Each solves a local problem.

The difficulty appears when the entire system is viewed as a whole.

Every local optimisation introduces another interaction.

Another dependency.

Another execution path.

Another configuration.

Another failure mode.

No single abstraction created the complexity.

The accumulation did.

This is one of the reasons mature architectures spend as much effort removing unnecessary abstractions as introducing new ones.

Complexity doesn’t usually appear because somebody made one poor architectural decision.

It appears because hundreds of individually reasonable decisions gradually become impossible to understand together.

Every Optimisation Has a Time Horizon

One of the reasons over-abstraction and under-isolation appear so consistently is that both optimise for today’s problems.

Abstraction reduces the effort required to build new features.

Shared infrastructure reduces operational cost.

Dependency injection simplifies testing.

Frameworks remove repetitive code.

Shared caches improve performance.

Shared connection pools reduce resource usage.

Every one of these decisions produces an immediate benefit.

The costs appear much later.

The abstraction makes debugging slower.

The shared resource becomes a shared failure domain.

The framework hides behaviour that engineers eventually need to understand.

The architecture hasn’t become worse.

The environment around it has changed.

What was once a development optimisation gradually becomes an operational liability.

Systems Mature Faster Than Their Architecture

Architectures are often designed for the organisation that exists today.

Production systems continue operating long after that organisation has changed.

New teams are created.

Products expand.

Traffic increases.

Compliance requirements appear.

Integrations multiply.

The architecture continues carrying assumptions made years earlier.

A shared cache that served two services now serves twenty.

A database that once handled operational data now supports analytics.

A messaging platform originally intended for asynchronous processing becomes the backbone of multiple business workflows.

None of these transitions are unusual.

The architecture simply survives long enough for its original assumptions to become increasingly unrealistic.

Architectural Drift

           Initial System

      Service A





      Shared Database

Simple

Understandable

Low Risk


────────────────────────────────


         Mature System

 Service A   Service B   Service C
      │          │          │
      └──────────┼──────────┘

         Shared Database

 Shared Cache

 Shared Queue

 Shared Identity

 Shared Connection Pool

Local optimisation has become
system-wide dependency.

The architecture didn’t suddenly become complicated.

It accumulated responsibility.

Coupling Is Usually Invisible Until Failure

One of the reasons coupling is difficult to manage is that healthy systems rarely expose it.

Every request succeeds.

Every dependency responds.

Every shared resource has spare capacity.

The coupling exists.

Nothing draws attention to it.

Production incidents change that.

A database slows down.

Suddenly every service waiting for database connections becomes slow.

A cache cluster becomes unavailable.

Applications that appeared independent all begin failing together.

A certificate expires.

Authentication fails across dozens of unrelated services.

The coupling was always present.

Failure simply made it visible.

This is one of the reasons resilience engineering focuses so heavily on failure exercises, and why distributed tracing matters once failure paths cross service boundaries.

Normal operation hides architectural assumptions.

Failure exposes them.

Failure Reveals Structure

        Normal Operation

 Service A      Service B

      ✓              ✓

Everything appears independent.


────────────────────────────────


          Database Failure

 Service A      Service B

      ✗              ✗

Shared dependency revealed.

One of the more useful outcomes of production incidents is that they often reveal architectural relationships nobody realised existed.

Architecture Shapes Debugging

Software engineers often think of architecture as something that influences maintainability.

It also determines how investigations unfold.

Suppose an API begins timing out.

In one architecture the engineer can inspect:

  • the service
  • its database
  • its queue

The investigation remains local.

Now imagine the same service sharing:

  • authentication
  • caching
  • messaging
  • connection pools
  • infrastructure proxies
  • observability agents

The investigation now spans half the platform, especially when metrics and logs tell different stories.

Nothing about the original failure became more technically complicated.

The architecture simply expanded the number of places where the explanation might exist.

This is one of the less obvious costs of hidden dependencies.

They don’t merely increase failure probability.

They increase investigative complexity.

Local vs Global Debugging

         Local Architecture

 Service





 Database

Investigation stays local.


──────────────────────────────


        Shared Platform

 Service





 Gateway

 Identity

 Cache

 Queue

 Proxy

 Database

 Investigation becomes systemic.

Good architecture doesn’t eliminate debugging.

It reduces the number of places engineers need to look.

Cognitive Load Is an Architectural Constraint

One idea that has become increasingly influential in modern software architecture is that systems should be designed around human understanding as much as computational efficiency.

Every additional abstraction increases the number of concepts an engineer must understand.

Every hidden dependency expands the mental model required to explain failures.

Every shared resource introduces another relationship that must be remembered during an incident.

Eventually the limiting factor isn’t CPU capacity.

It’s cognitive capacity.

Engineers don’t struggle because systems contain too many components.

They struggle because understanding one component increasingly requires understanding twenty others.

Architecture therefore becomes an exercise in managing attention.

Not simply computation.

This changes how abstraction should be evaluated.

The question isn’t:

“Does this remove code?”

It’s:

“Does this reduce the amount an engineer must understand to make a correct decision?”

Sometimes the answer is yes.

Increasingly, as systems mature, the answer becomes no.

Good Architecture Optimises for Understanding

Software architecture is often evaluated by qualities such as flexibility, reuse and scalability.

Those qualities matter.

There’s another quality that’s discussed far less often.

Understanding.

How quickly can an engineer explain what this system is doing?

How many components must they understand before making a safe change?

How many services must they inspect before identifying the source of a production incident?

Those questions become increasingly important as systems mature.

A platform that scales to millions of requests provides little value if nobody can confidently explain why it failed.

Good architecture reduces the amount of understanding required to make correct decisions.

It doesn’t eliminate complexity.

It places that complexity where it can be managed.

Boundaries Should Match Failure Domains

One of the recurring themes throughout this article is that architectural boundaries often reflect development concerns rather than operational ones.

Repositories organise persistence.

Controllers organise HTTP requests.

Services organise business logic.

Those are useful abstractions.

Production systems fail differently.

Failures occur around:

  • databases
  • queues
  • caches
  • identity providers
  • third-party APIs
  • storage systems
  • network boundaries

These are failure domains.

When software boundaries ignore operational boundaries, debugging becomes significantly more difficult.

The code suggests one structure.

Production behaves according to another.

Successful architectures increasingly align these two perspectives.

The units engineers develop become the same units they monitor, deploy and recover.

Aligning Software and Failure Boundaries

          Development View

 Controller


 Service


Repository

Organised by code.


────────────────────────────────


          Operational View

 Service


Database

 Service


Queue

 Service


External API

Organised by failure domains.

The closer these views become, the easier production behaviour is to understand.

Simplicity Is a Property of Operations

Software often appears simple during development because most assumptions continue to hold.

Dependencies are available.

Traffic is predictable.

Resources are plentiful.

Configuration is correct.

Production removes those assumptions.

Network latency appears.

Infrastructure becomes unavailable.

Queues fill.

Certificates expire.

Resource limits are reached.

The architecture that appeared straightforward under ideal conditions suddenly reveals all of its hidden behaviour.

This is why operational simplicity is often very different from implementation simplicity.

A service containing more explicit code may prove easier to understand than one relying heavily on framework conventions.

An isolated component may require additional infrastructure while dramatically reducing the effort required to diagnose failures.

Simple code doesn’t necessarily produce simple systems.

Operational simplicity depends on whether behaviour remains understandable when the environment becomes unpredictable.

Visibility Is More Valuable Than Elegance

Many architectural patterns optimise for elegance.

Minimal code.

Generic abstractions.

Reusable components.

Implicit configuration.

These qualities make systems pleasant to build.

They don’t necessarily make systems pleasant to operate.

Production engineering places a premium on visibility.

Visible dependencies.

Visible retries, visible correlation IDs.

Visible timeouts.

Visible ownership.

Visible failure boundaries.

Visible resource sharing.

These details occasionally make the implementation appear less elegant.

They almost always make the behaviour easier to explain.

Architecture isn’t judged during code review.

It’s judged during incidents.

That’s when visibility becomes more valuable than elegance.

Visible Behaviour

          Explicit Design

 Service



 Retry Policy

 Timeout

 Circuit Breaker

 Cache



 Database

Operational behaviour is visible.


────────────────────────────────


         Hidden Behaviour

 Service



 Framework



 Database

Behaviour exists.

Engineers must discover it.

A small amount of explicitness often removes hours of investigation later.

The Best Systems Are Boring to Debug

Experienced engineers often describe well-designed systems as boring.

That’s usually a compliment.

When something fails, they already know where to look.

The failure remains contained.

Dependencies are obvious.

Ownership is clear.

Recovery procedures are predictable.

Nothing surprising happens.

Contrast this with systems where every incident becomes an investigation into hidden dependencies, undocumented retries and unexpected interactions between components.

Those systems aren’t necessarily more complicated.

They’re simply less understandable.

One of the strongest indicators of architectural quality isn’t how sophisticated the implementation appears.

It’s how unsurprising failures become.

Final Thoughts

Software architecture is frequently presented as a way of managing complexity.

After working with production systems, I think that’s only part of the story.

Complexity never really disappears.

Every abstraction hides it.

Every framework relocates it.

Every shared dependency redistributes it.

The important architectural question isn’t whether complexity exists.

It’s whether the people operating the system can still see the parts that matter.

Good abstractions hide mechanics without hiding behaviour.

Good isolation limits failures without obscuring dependencies.

Good boundaries reduce the amount of knowledge required to understand a problem.

Ultimately, architecture isn’t judged by how elegant the diagrams appear or how many patterns the implementation follows, the same lesson behind systems that outlive their platforms.

It’s judged by what happens on the worst day.

When production is failing, engineers don’t need another layer of abstraction.

They need enough visibility to understand what the system is actually doing.

The most resilient architectures aren’t the ones with the fewest components.

They’re the ones that make complexity visible in the places where understanding matters most.