Abstraction and Interfaces

Introduction

A payment service at a large e-commerce company calls chargeCard(amount, token). The engineers who wrote that call have no idea whether the underlying processor is Stripe, Braintree, or an internal gateway. They do not know whether the charge goes over REST or gRPC. They do not know whether there is a retry queue behind the scenes. They know one thing: they pass an amount and a token, and they get back a result. Three years later, the company switches processors. The payment service code does not change. That is what a good abstraction achieves.

Now consider the alternative. A different team wrote chargeStripe(amount, stripeToken, idempotencyKey, retryCount). When the processor changes, every caller changes. The implementation detail — Stripe — leaked through the interface into every system that depended on it. The abstraction failed.

Thread Activation

You have already seen encoding (T10) in Book 1 and Book 2, where data is transformed into a representation that hides internal structure — a hash hides the raw data, a compressed format hides the original bytes. Here the same pattern appears at code level. An abstraction encodes a capability: it presents a consistent interface that hides the complexity of implementation behind a stable surface. The shape is the same: what varies is concealed, what is stable is exposed.

The Concept

An abstraction is a simplified representation of something more complex. In code, an abstraction takes the form of an interface: a defined set of operations a component exposes, with no commitment about how those operations are implemented.

Three properties make an abstraction good. First, it hides irrelevant complexity. The caller of chargeCard does not need to know about retry logic, idempotency keys, or processor-specific error codes. Hiding this complexity is the primary job of the interface. Second, it exposes only what callers need. Every additional method or parameter in an interface is a commitment. Commitments are expensive — they constrain every future implementation. Third, it does not leak implementation details.

The Law of Leaky Abstractions states that every non-trivial abstraction eventually reveals implementation details. A database abstraction layer returns connection pool errors. A filesystem abstraction reveals OS-level path separators. A cloud storage abstraction exposes eventual consistency. The law does not mean abstractions are useless — it means abstractions require maintenance and that callers must be prepared for leakage under stress.

Interface design follows from these properties. A good interface is small (few methods), stable (it rarely changes), and complete (it supports everything callers legitimately need). An interface with twenty methods is either poorly bounded or covering too many responsibilities. An interface that changes every release forces every caller to change.

The cost of a bad abstraction is paid in coupling. When an implementation detail leaks through an interface, callers begin to depend on that detail. The implementation can no longer change without changing callers. What was meant to isolate change instead propagates change.

How It Works

Consider a storage abstraction:

interface BlobStore:
    method put(key: String, data: Bytes) -> Result
    method get(key: String) -> Result<Bytes>
    method delete(key: String) -> Result
    method exists(key: String) -> Boolean

Two implementations:

class LocalFileBlobStore implements BlobStore:
    method put(key, data):
        path = join(base_dir, key)
        write_file(path, data)
        return Success

    method get(key):
        path = join(base_dir, key)
        if not file_exists(path):
            return NotFound
        return read_file(path)
class S3BlobStore implements BlobStore:
    method put(key, data):
        s3_client.put_object(bucket, key, data)
        return Success

    method get(key):
        try:
            return s3_client.get_object(bucket, key)
        catch NoSuchKey:
            return NotFound

Callers depend on BlobStore. The choice of local disk or S3 is a deployment decision invisible to callers. Testing uses LocalFileBlobStore or an in-memory stub. Production uses S3BlobStore. The abstraction holds.

Where does it break? If S3BlobStore.put occasionally returns EventuallyConsistent — an S3-specific result type — and callers start to handle it, the abstraction has leaked. Now callers know about S3. Removing S3 requires changing callers. The interface has become a lie.

Tradeoffs

AT8 — Coupling/Cohesion: A well-designed abstraction reduces coupling by hiding implementation. Callers depend on the interface, not the implementation. This means implementations can change independently. The cost: the interface itself becomes a coupling point. Changing an interface requires changing every implementor and every caller. Good interfaces are therefore conservative — add methods rarely, remove methods never.

AT3 — Simplicity/Flexibility: A narrow interface is simple. A wide interface is flexible but harder to implement and harder to reason about. A BlobStore with four methods is easy to implement against any storage backend. A BlobStore with twenty methods — supporting metadata, tagging, versioning, lifecycle policies — is flexible but forces every implementor to cover the full surface, including implementors that only need the basics.

Where It Fails

FM8 — Schema/Contract Violation: When an interface changes without coordinating with callers, behaviour breaks at the boundary. A method renamed, a return type changed, an error semantics altered — these are contract violations. Callers compiled against the old contract fail at runtime. In loosely-typed systems or across service boundaries, these violations are silent until production.

FM2 — Cascading Failures: A leaky abstraction that exposes retry logic or timeout semantics can cause callers to implement their own retry strategies on top of the implementation’s retry strategies. Double-retry under failure amplifies load rather than protecting against it. The abstraction intended to simplify behaviour accidentally produced emergent failure behaviour.

Real Systems

POSIX filesystem interface: open, read, write, close — four operations that have abstracted every storage medium from magnetic tape to NVMe SSDs for fifty years. Implementations vary enormously; the interface has not changed. This is the canonical example of a stable, well-bounded abstraction.

JDBC (Java Database Connectivity): A standard interface for relational databases. Write once against Connection, Statement, ResultSet; run against MySQL, PostgreSQL, or Oracle. Leakage appears in SQL dialects (the abstraction does not cover query language) and in connection pool errors (implementation-specific failure modes surface through the generic interface).

Kubernetes API: kubectl apply submits a desired state manifest. The control plane — which may run on any cloud or on bare metal — reconciles reality toward that state. Callers know nothing about the underlying infrastructure. The abstraction is so successful that the same manifest deploys to AWS, GCP, or an on-premises cluster.

HTTP: The protocol abstracts TCP. Callers speak HTTP methods and status codes; they do not manage TCP handshakes or retransmission. Leakage occurs at connection limits, timeout configurations, and keep-alive behaviour — TCP details that surface under load.

Concept: Abstraction and Interfaces

Thread: T10 (Encoding) ← Book 1, Ch 9 (Hashing as data abstraction) → Ch 17 (API Design and Versioning)

Core Idea: An abstraction hides irrelevant complexity behind a stable interface; callers depend on the interface, not the implementation, so implementations can change independently.

Tradeoff: AT8 — Coupling/Cohesion: the interface reduces coupling to the implementation but creates coupling to itself; change the interface and every caller and implementor must change.

Failure Mode: FM8 — Schema/Contract Violation: interfaces that change without coordination, or that leak implementation details, break callers at the boundary.

Signal: When changing a subsystem requires changing callers that should not care about implementation details, the abstraction is either missing or leaking.

Maps to: Book 0, Framework 8 (Patterns); P1 (Abstraction)

Exercises

Level 1 — Understand

1. What are the three properties that make an abstraction good, as described in this chapter?

2. What does the Law of Leaky Abstractions state, and what does it imply for interface design?

3. Name two real-world examples from the chapter where an abstraction has survived significant underlying change, and identify what made each interface stable.

Level 2 — Apply

  1. A team exposes a UserRepository interface with methods findById, findByEmail, save, delete, and findAllByCreatedDateBetween. Which methods are likely to leak implementation details? Rewrite the interface to expose only what callers need, and describe what you removed and why.

  2. A logging abstraction currently defines log(level, message, timestamp, hostname, pid). A new implementation — a cloud logging service — does not use pid or hostname (it derives them automatically). What does this tell you about the interface design? Propose a corrected interface and explain the tradeoff.

  3. Two services share a PaymentResult type defined in a shared library. Team A adds a new field processorFee to the type and deploys. Team B has not deployed yet. Describe exactly what breaks and which failure mode this represents.

Level 3 — Design

  1. Design a CacheStore interface that could be implemented by an in-memory LRU cache, Redis, and a distributed consistent cache (like etcd). Identify which operations are safe to include in a shared interface, which are implementation-specific and must be excluded, and where leakage is inevitable under failure scenarios. Name the tradeoffs using AT codes.

A complete answer will: (1) define a minimal safe interface (get, set, delete, exists) and justify why distributed-specific operations (e.g., distributed locking, watch/notify, CAS) must be excluded — callers that assume these operations exist cannot safely use the in-memory implementation, violating the Liskov Substitution Principle, (2) name AT3 (Simplicity/Flexibility): a minimal interface is easy for all implementations to satisfy but forces callers to work at the lowest common denominator; a richer interface enables callers to use more powerful features but narrows the set of valid implementations, (3) identify where leakage is inevitable: failure semantics differ across implementations — an in-memory LRU never throws a network error, Redis may throw a connection timeout, etcd may throw a quorum failure; a shared interface cannot fully abstract these differences, and callers must either handle implementation-specific exceptions or the interface must define a unified exception hierarchy, and (4) name FM1 (single point of failure) for the Redis implementation (a single Redis node behind the interface is a SPOF that the in-process LRU never is) and FM12 (network partition) for etcd — state that an abstraction hiding these failure modes from callers produces callers that cannot handle the failures correctly.

  1. A company is migrating from one payment processor to another. The existing code calls the old processor’s SDK directly at forty call sites. Design a migration strategy using abstraction that allows the migration to happen incrementally, without a flag day, and without modifying all forty call sites at once. Name the pattern, describe the interface, and identify the failure modes during the migration window.

A complete answer will: (1) name the Façade (or Adapter) pattern and describe a PaymentProcessor interface that wraps both the old and new processor SDKs — the forty call sites are updated once to call the interface instead of the old SDK directly, and the interface implementation routes to old or new processor based on a feature flag, (2) design the incremental migration: route a configurable percentage of transactions to the new processor (e.g., 1%, 10%, 50%, 100%) with the ability to roll back to 0% without a code deployment — specify the AT3 tradeoff (the routing layer adds code complexity but eliminates flag-day risk), (3) name FM2 (cascading failure) during the migration window: if the new processor’s API returns unexpected errors, the calling code must not crash — the interface must implement a fallback that retries on the old processor for recoverable errors, with the AT1 tradeoff (consistency of which processor charged the card vs. availability of the transaction) stated explicitly, and (4) identify the FM4 risk (stale data / dual-write inconsistency): during the window when both processors are active, refund and dispute queries must check both processor histories — propose a transaction log that records which processor handled each charge, enabling correct routing of refunds.