DEV Community

Cover image for Abstraction: Designing Systems That Don’t Collapse Under Complexity
Walter Nascimento
Walter Nascimento

Posted on

Abstraction: Designing Systems That Don’t Collapse Under Complexity

Encapsulation protects invariants.

Abstraction protects architecture.

If encapsulation controls state,
abstraction controls dependency.

And without it, your system slowly turns into a fragile web of concrete implementations.


A Brief Historical Context

Abstraction became critical when software systems stopped being small.

In early OOP systems, objects communicated directly with concrete implementations.

But as systems grew:

  • Infrastructure changed
  • Databases evolved
  • APIs were replaced
  • Vendors switched

Hard-coded dependencies became the biggest source of rigidity.

Abstraction emerged as a way to:

Depend on behavior contracts, not implementations.

That single idea made large systems survivable.


What Abstraction Really Means

Abstraction is:

  • Defining behavior without exposing implementation
  • Programming against contracts
  • Isolating high-level logic from low-level details
  • Reducing coupling

Abstraction is not:

  • Just creating interfaces everywhere
  • Adding layers for no reason
  • Over-engineering small systems

Abstraction is about managing volatility.


❌ The Problem: Concrete Coupling

Let’s say we’re building a payment service.

final class OrderService
{
    public function pay(float $amount): void
    {
        $stripe = new StripePaymentGateway();
        $stripe->charge($amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

What’s wrong?

  • OrderService depends directly on Stripe
  • Impossible to switch provider without editing business logic
  • Hard to test
  • Violates dependency inversion
  • Infrastructure leaks into domain logic

This is tight coupling.


Why This Becomes Dangerous

Imagine:

  • Stripe increases fees
  • You must support PayPal
  • A region requires a local provider
  • Stripe API changes

Now you must modify core logic.

Your domain is polluted by infrastructure decisions.

That’s architectural fragility.


✅ Introducing Abstraction

We define a contract.

interface PaymentGateway
{
    public function charge(float $amount): void;
}
Enter fullscreen mode Exit fullscreen mode

Now we create implementations.

final class StripePaymentGateway implements PaymentGateway
{
    public function charge(float $amount): void
    {
        // Call Stripe API
    }
}
Enter fullscreen mode Exit fullscreen mode
final class PaypalPaymentGateway implements PaymentGateway
{
    public function charge(float $amount): void
    {
        // Call PayPal API
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we depend on abstraction.


Refactoring the Service

final class OrderService
{
    public function __construct(
        private PaymentGateway $paymentGateway
    ) {}

    public function pay(float $amount): void
    {
        $this->paymentGateway->charge($amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now:

✔ OrderService does not know about Stripe
✔ Infrastructure can change
✔ Testing becomes trivial
✔ Domain logic is protected

This is abstraction.


Abstraction and Dependency Direction

The critical rule:

High-level modules should not depend on low-level modules.
Both should depend on abstractions.

This is the foundation of the Dependency Inversion Principle.

Your domain should never depend on Stripe.

Stripe should depend on your abstraction.


Architectural Impact

With abstraction:

  • You can switch providers
  • You can mock dependencies
  • You isolate external volatility
  • You maintain stable core logic

Without abstraction:

  • Refactoring becomes dangerous
  • Tests require real infrastructure
  • Every change ripples across the system

Advanced Insight: Abstraction is About Volatility Isolation

Ask yourself:

What changes frequently?

  • External APIs
  • Databases
  • Message brokers
  • File storage
  • Email providers

These are volatile dependencies.

Your core business rules are stable.

Abstraction isolates stable from unstable.


When Abstraction Is Overkill

Abstraction adds indirection.

Do not abstract:

  • Simple scripts
  • Tiny applications
  • One-off utilities
  • Stable internal code with no variability

Premature abstraction creates:

  • Interface pollution
  • Unnecessary complexity
  • Cognitive overhead

Abstraction should follow variability, not fear.


Abstraction vs Encapsulation

Encapsulation protects internal state.

Abstraction protects dependency boundaries.

Encapsulation works inside the object.

Abstraction works between objects.

Together, they define structural integrity.


Common Anti-Patterns

1️⃣ Interface Per Class

interface UserServiceInterface {}
class UserService implements UserServiceInterface {}
Enter fullscreen mode Exit fullscreen mode

No variability.

No alternative implementation.

Just noise.


2️⃣ Leaky Abstractions

interface PaymentGateway
{
    public function chargeWithStripeToken(string $token): void;
}
Enter fullscreen mode Exit fullscreen mode

Stripe-specific logic inside abstraction.

That defeats the purpose.


Final Insight

Abstraction is not about hiding code.

It is about controlling dependency direction.

If encapsulation prevents internal chaos,
abstraction prevents architectural collapse.

Abstraction allows your system to evolve without rewriting its core.

That is why it is the second pillar of OOP.


Thanks for reading!

If you have any questions, complaints or tips, you can leave them here in the comments. I will be happy to answer!
😊😊 See you! 😊😊


Support Me

Youtube - WalterNascimentoBarroso
Github - WalterNascimentoBarroso
Codepen - WalterNascimentoBarroso

Top comments (0)