Interface inheritance
Software design
OOP
Programming best practices
Software architecture

Should one interface inherit another interface

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Yes, one interface can and often should inherit another interface, but only when the derived interface is truly a more specific contract. Interface inheritance is a modeling tool, not a convenience feature. If the relationship is weak or accidental, it usually creates unnecessary coupling and pushes unrelated methods onto implementers.

When Interface Inheritance Makes Sense

A good rule is simple: if every implementation of the child interface must also satisfy the parent interface, inheritance is appropriate.

A classic example is a readable stream and a seekable readable stream:

java
1public interface Reader {
2    String read();
3}
4
5public interface SeekableReader extends Reader {
6    void seek(long position);
7}

This models a clear subtype relationship. Anything that is a SeekableReader is also a Reader, so code that only needs Reader can accept the more specific type without knowing about seeking.

That gives you two benefits:

  • shared vocabulary for the common contract
  • stronger type information for specialized consumers

For example:

java
1public void printChunk(Reader reader) {
2    System.out.println(reader.read());
3}
4
5public void rewindAndPrint(SeekableReader reader) {
6    reader.seek(0);
7    System.out.println(reader.read());
8}

This is exactly what interfaces are good at: expressing behavior in layers of increasing specificity.

When It Is a Design Smell

The trouble starts when an interface inherits another one merely to avoid repetition or to force unrelated capabilities together.

Suppose you write this:

java
1public interface Logger {
2    void log(String message);
3}
4
5public interface Cache extends Logger {
6    String get(String key);
7    void put(String key, String value);
8}

That design says every cache is also a logger. That is rarely true. Logging may be a feature of one implementation, but it is not a defining part of what a cache is. The inheritance expresses the wrong semantic relationship.

In cases like that, keep the contracts separate:

java
1public interface Logger {
2    void log(String message);
3}
4
5public interface Cache {
6    String get(String key);
7    void put(String key, String value);
8}

A class can still implement both interfaces when needed, but the type system no longer lies about the domain model.

Favor Small, Focused Contracts

This idea lines up with the Interface Segregation Principle. Small interfaces are easier to compose than large inherited hierarchies.

In C#, for example:

csharp
1public interface IReadable
2{
3    string Read();
4}
5
6public interface IWritable
7{
8    void Write(string value);
9}
10
11public interface ISeekable
12{
13    void Seek(long position);
14}
15
16public class FileChannel : IReadable, IWritable, ISeekable
17{
18    public string Read() => "data";
19    public void Write(string value) { }
20    public void Seek(long position) { }
21}

Here the class composes several focused capabilities without needing an artificial inheritance chain between the interfaces. That tends to stay flexible as the system evolves.

A Useful Decision Test

Before making one interface extend another, ask:

  1. Is the child always a valid substitute for the parent
  2. Would every implementation of the child naturally support every member of the parent
  3. Does the inheritance communicate domain meaning, not just code reuse

If the answer to any of these is no, composition is usually better than inheritance.

Versioning and API Stability

Interface inheritance also affects public API evolution. If you publish a base interface and many implementations depend on it, adding members to that base can ripple across your codebase or external consumers.

That means inheritance should be used carefully in shared libraries. A narrow base contract stays stable longer. A deep hierarchy can make even small changes expensive.

Sometimes it is safer to add a new sibling interface than to expand an inherited base contract. That lets callers opt into new behavior without breaking existing implementers.

Common Pitfalls

The biggest pitfall is inheriting for convenience instead of semantics. Removing duplicated method signatures feels nice in the moment, but the result can encode a false subtype relationship.

Another issue is building deep interface hierarchies too early. A couple of levels may be reasonable, but large inheritance trees make it harder to understand what a type actually promises.

Developers also sometimes use inheritance when multiple interface implementation would be clearer. If a class can read and write, that does not automatically mean Writable should extend Readable or the other way around.

Finally, think about consumers, not just implementers. If most callers only need a tiny contract, exposing a large inherited interface makes testing and substitution harder than necessary.

Summary

  • An interface should inherit another interface only when it represents a true subtype.
  • Inheritance is appropriate for specialization, not just for avoiding repeated method signatures.
  • Separate interfaces and multiple implementation are often better for unrelated capabilities.
  • Small, focused contracts usually age better than deep interface hierarchies.
  • If the domain meaning is unclear, prefer composition over interface inheritance.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.