Why Your Software Sucks: Inheritance

Deep inheritance hierarchies create 'everything touches everything' architectures that resist change and accumulate technical debt. Here's why inheritance is the gateway drug to frameworkism.

“I have a great idea: let’s create a five-layer deep inheritance hierarchy with a universal base class that every domain object inherits from! That way, when requirements inevitably change, we’ll only need to touch… everything.”

This is episode two of my Why Your Software Sucks video series, and today we’re talking about inheritance - specifically, how deep inheritance hierarchies turn your codebase into an unmaintainable mess.

The Two Deadly Sins of Deep Inheritance

Sin #1: Cognitive Overload

Once you get beyond a base class plus implementers - say, four or five layers deep - tracing behavior becomes a nightmare. “Why did X do Y?” requires stepping through multiple layers of code, each with its own internal state. The cognitive load multiplies with every additional layer.

Sin #2: The “Everything Touches Everything” Architecture

Someone decides to create a universal model for how things should behave across your entire domain. This almost always fails, even in expert hands, unless:

Those are rare circumstances. Most of us aren’t that good at predicting the future.

Configuration Toggle Hell

When your universal base class stops fitting actual domain needs, that’s when the configuration toggles start creeping in:

public abstract class UserServiceStartup : ServiceStartupWithDatabase<UserServiceDbContext>
{
    protected override bool EnableBearerHeaderInSwagger => true;
    protected override bool EnableAuthorization => true;
    protected override string DatabaseConnectionStringName => "UserService";
    protected override bool UseLowercaseRouting => true;
    // ... and on and on
}

Why do I have five layers of base classes to configure a simple ASP.NET Core application? Because someone fell into the trap of preemptive framework design - trying to rigidly design how software should work before it makes contact with reality.

A Real-World Horror Story

Let me show you what a five-layer inheritance hierarchy looks like in practice. This is from our own codebase at Sdkbin - a project I’ve written about before in terms of the management failures that allowed its technical debt to accumulate unchecked for years. Here’s a taste of what I found when I finally started excavating:

The inheritance hierarchy in question:

graph TD
    A[BuyPage.cshtml.cs] --> B[CustomerPageBase]
    B --> C[OrganizationPageModel]
    C --> D[PageBaseModel]
    D --> E[PageModel - ASP.NET Core]

Five layers. Five. To deliver a single page that processes Stripe payments.

The middle layer - OrganizationPageModel - injects repositories, authorization services, and host environment dependencies. It enforces opinions about how authentication should work, how organizations should be loaded, and what database round trips are necessary.

This creates massive friction when we try to migrate from CRUD repositories to CQRS. Every single page that inherits from this base class expects its organization preloaded in advance. Changing this requires either:

  1. Rewriting the base class - high blast radius, potentially nukes all downstream dependencies
  2. Migrating away entirely - which is what we’re doing, ditching Razor Pages for MVC controllers

We never should have had five layers in the first place.

The 866-Line Generic Repository of Doom

The other inheritance anti-pattern I want to highlight: the dreaded Generic Repository.

Our RepositoryBase<TEntity, TContext> class is 866 lines of highly opinionated code that:

This is frameworkism in action - the fatal conceit that you can model an entire business domain with a single rigid abstraction.

Organizations aren’t single rows. They’re multifaceted objects with members, addresses, billing information, and complex authorization rules. When the base class can’t express what we need, we’re forced to add private helper methods that work around the abstraction:

private IQueryable<Organization> GetOrganizationsQueryable()
{
    // Because the base class can't handle this...
}

Ironic, isn’t it?

The Gateway Drug to Frameworkism

Inheritance becomes dangerous when developers start using it preemptively - creating elaborate base class hierarchies before serving a single live request. This is a symptom of three fatal framework conceits:

  1. Preemption - building the framework before solving real problems;
  2. Rigidity - universal models that can’t accommodate different requirements; and
  3. Predetermination - using the same tools for every problem, regardless of context.

Inheritance enables all three. It makes you feel smart while you’re designing in a vacuum, before reality has a chance to prove your universal model wrong.

What to Do Instead

Extract, don’t preempt. Build frameworks after you’ve solved the same problem three different ways in your application. Then you’ll know where the real commonalities are, and you can create thin abstraction layers that actually help.

Prefer composition over inheritance. Compose the different components that make up your feature at runtime rather than baking dependencies into deep class hierarchies.

Be skeptical of depth. When you start seeing deep inheritance hierarchies, that’s usually a sign something has gone wrong. There are legitimate exceptions - ASP.NET Core controllers and Akka.NET actors benefit from deeper hierarchies because they’re internalizing infrastructure concerns. But those are the exceptions, not the rule.

Inheritance isn’t universally evil. But if you find yourself reaching for another layer of base classes to model your domain, stop. You’re probably about to make a mistake that’ll haunt you for years - I know from experience.


This post is a recap of the video above. What’s the deepest inheritance hierarchy you’ve ever encountered in a codebase? I’ve seen 7+ layers in the wild. Drop your horror stories in the YouTube comments - bonus points if it involved a generic repository.

Discussion, links, and tweets

I'm the CTO and founder of Petabridge, where I'm making distributed programming for .NET developers easy by working on Akka.NET, Phobos, and more..