Hermes Wiki

Interfaces vs Abstract Classes

Concept

Both interfaces and abstract classes let you define a contract that concrete classes must fulfill without you having to say up front which concrete class will fulfill it — the prerequisite for almost every structural/behavioral design pattern (Strategy, Adapter, Observer all depend on it). The difference is what each is allowed to carry alongside that contract.

An interface declares method signatures with no implementation and, traditionally, no state — it's a pure "what can you do" contract. A class can implement any number of interfaces, because agreeing to fulfill several unrelated contracts doesn't create the structural ambiguity that inheriting implementation from several places does.

An abstract class can mix declared-but-unimplemented methods with real, shared implementation and instance state — a partial blueprint rather than a pure contract. Because it can carry actual field state and constructor logic, most mainstream OO languages restrict a class to extending exactly one abstract class (single inheritance), even though it can still implement many interfaces at the same time.

Many modern languages have blurred this line further: Java and C# interfaces can now carry default method bodies, and Python has no compiler-enforced interface construct at all — abc.ABC gives an inheritance-based abstract base class, while typing.Protocol gives structural typing (a class satisfies the "interface" just by having the right methods, no explicit implements/inheritance declaration needed). TypeScript's interface is structural in the same sense. The underlying distinction (pure contract vs partial-implementation blueprint) still holds even where the syntax overlaps.

Tradeoffs

Construct Benefit Cost
Interface (no default methods) A class can satisfy many at once — no single-inheritance limit; forces callers to depend only on behavior, never on implementation details No code reuse — every implementer rewrites the same logic if multiple implementations share behavior
Interface with default methods (Java 8+/C#) Lets an interface evolve (add a method with a default body) without breaking every existing implementer Blurs the "pure contract" guarantee — an interface can now carry real behavior, reintroducing some of the diamond-inheritance questions interfaces were meant to avoid
Abstract class Can share real implementation and state across subclasses, not just a signature; constructor logic runs for every subclass Single-inheritance slot spent — a class can extend only one abstract class, so choosing one blocks reuse of another abstract class's shared logic
Structural typing (Python Protocol, TypeScript interface) No explicit implements coupling — any object with matching methods satisfies the contract, including ones defined before the protocol existed Contract is checked structurally (does it have the right method names/shapes), not by intent — nothing stops something that "looks like" the protocol by accident, and no compiler enforcement in Python without a static checker (mypy/pyright) actually running

The practical decision usually reduces to one question: does this contract need to carry any shared implementation or state? If no, an interface (or structural protocol) is strictly less coupling for the same guarantee. If yes — if subclasses genuinely share logic, not just a signature — an abstract class avoids duplicating that logic across every implementer, at the cost of the single-inheritance slot.

When to use / when not to

  • Use an interface when multiple, otherwise-unrelated classes need to satisfy the same contract with no shared implementation — e.g. Serializable, Comparable, a PaymentProvider interface implemented by unrelated StripeProvider and PaypalProvider classes.
  • Use an abstract class when subclasses share real, non-trivial implementation or state alongside the contract — e.g. a BaseRepository abstract class providing common CRUD scaffolding that concrete repositories extend and only override a few methods on.
  • Prefer structural typing (Python Protocol, TypeScript's structural interfaces) over an explicit abstract base class specifically when you don't want implementers coupled to an inheritance hierarchy at all — this is the right default when the "contract" is really just "has these methods," and third-party or pre-existing classes need to satisfy it without being rewritten to inherit from anything.
  • Don't reach for an abstract class purely to get code reuse when there's no genuine is-a/contract relationship — that's the same "inheritance for convenience" mistake covered in Composition vs Inheritance, just at the abstract-class level instead of the concrete-class level.
  • Don't add default methods to an interface just to sneak in shared logic — if there's real shared implementation, that's a signal an abstract class (or a composed helper object) is the honest structure, not an interface stretched past its contract-only design intent.

Common pitfall

Choosing an abstract class purely for the shared-code convenience and only later discovering a subclass legitimately needs to share implementation from a second abstract class too — which the single-inheritance limit makes impossible without refactoring. The usual fix bends the design further: pulling the second class's logic into composed helper objects, or flattening the hierarchy, after the fact — expensive changes that a small amount of up-front thought (does this subclass actually need to be this abstract type, or just use some of its behavior?) would have avoided. The same question that separates composition from inheritance applies one level up, when picking abstract class vs interface.

Engineering Lens

The design-review question isn't "does this need multiple methods declared together" — almost anything qualifies for that. It's "does this contract carry shared implementation or state that every conforming type would otherwise have to duplicate?" If the honest answer is no, an interface (or a structural protocol, if the language supports one) is strictly the better default: it gives every implementer full freedom to satisfy the contract however it needs to, with no inheritance-slot cost. Reaching for an abstract class when an interface would do isn't wrong in the sense of failing to compile — it's wrong in the sense of spending a scarce resource (the single-inheritance slot) for a benefit (shared implementation) the design doesn't actually need yet, foreclosing a future refactor that would have needed that slot for something real.

  • Composition vs Inheritance — the same fragile-base-class and single-inheritance-slot tradeoffs apply to abstract classes, one level up from concrete inheritance
  • Adapter Pattern — a canonical pattern that depends entirely on defining a contract (interface) separately from any concrete implementation

Sources

Hermes Wiki