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.