DEV Community

Cover image for SOLID isn't overrated, it's misapplied
Matías Denda
Matías Denda

Posted on

SOLID isn't overrated, it's misapplied

Another article about SOLID. Original, I know.

But I want to discuss something different from the usual letter-by-letter explanation: why entire ecosystems — Node.js, Kotlin/Android, parts of the Python world — treat SOLID as a relic of Java enterprise that's better avoided. And why I think that reading is wrong.

The problem isn't SOLID, it's the cargo cult

When someone says "SOLID is overrated," nine times out of ten, they're not attacking the principles. They're attacking the caricature: five layers of abstraction for a CRUD, an IUserRepositoryFactory that returns a UserRepositoryFactory that builds a UserRepository, interfaces declared "just in case we need them someday," dependency injection containers to resolve a pure function.

That's not SOLID. That's overengineering wearing SOLID vocabulary. The distinction matters because when you discard the principles along with their misapplication, you end up making the opposite mistakes: 2000-line files, classes that do seven things, and code that's impossible to test because business logic is coupled to the HTTP framework.

The cultural bias by language

This is where it gets interesting. The same principle — say, Single Responsibility — produces different reactions depending on the ecosystem, and that has less to do with the language itself than with the culture built around it.

Node.js / Express. Open any mid-sized Express project, and you'll likely find a routes.js with 800 lines mixing route definitions, validation, business logic, and database queries. It's not that SRP "doesn't apply" in JavaScript. It's that the "move fast" culture treats it as an unnecessary ceremony — until the file becomes unmaintainable and subtle bugs from inconsistently duplicated validation start appearing. A quick look at popular Express tutorials makes this pattern clear: many introduce route handlers with inline business logic and only mention separation of concerns as an advanced topic, if at all.

Kotlin / Android. Particularly interesting case. The language gives you data class, sealed classes, extension functions, top-level functions — tools tailor-made for SRP and ISP without Java's verbosity. And yet, "God ViewModels" — ViewModels mixing UI logic, network calls, DTO mapping, and caching — are common enough that Google's own architecture guide for Android explicitly warns against them and recommends splitting responsibilities into dedicated classes. The language enables doing it well; many tutorials and starter templates don't model that.

Go (positive contrast). Go is one of the few ecosystems where the culture embraces ISP and SRP as idiomatic, without anyone calling it "SOLID." The proverb "the bigger the interface, the weaker the abstraction" — attributed to Rob Pike — is ISP in another language. Single-purpose packages, small interfaces defined on the consumer side, composition over inheritance — all SOLID, no ceremony.

Idiomatic SOLID ≠ SOLID in Java

The central misunderstanding is assuming SOLID demands the format it was originally taught with: classes, explicit interfaces, and hierarchies. It doesn't.

SRP doesn't require one class per responsibility. It requires one reason to change. In TypeScript, that can be a pure function:

// This satisfies SRP perfectly
export function calculateShippingCost(order: Order, rates: ShippingRates): Money {
  // one responsibility, one reason to change
}
Enter fullscreen mode Exit fullscreen mode

DIP doesn't require a DI container with XML config. A function parameter is already dependency inversion:

// The dependency is a parameter, not an import. That's DIP.
async function processPayment(
  order: Order,
  charge: (amount: Money) => Promise<Receipt>
): Promise<OrderResult> {
  const receipt = await charge(order.total);
  return { order, receipt };
}
Enter fullscreen mode Exit fullscreen mode

OCP in Go looks like this, with no inheritance:

// Adding a new connector requires no changes to existing code.
// Just implement the interface.
type Connector interface {
    Execute(ctx context.Context, input []byte) ([]byte, error)
}
Enter fullscreen mode Exit fullscreen mode

Three languages, three syntaxes, same principles. What changes is the implementation; what stays is the intent.

A heuristic

Before dismissing SOLID in your stack, ask yourself: Am I rejecting the principle, or a Java 2008 enterprise implementation that scarred me years ago?

If your routes file has 800 lines mixing five responsibilities, you're not being pragmatic. You're being disorganized and calling it pragmatism. If you have an AbstractFactoryBuilderStrategy to build a config object, you're not applying SOLID. You're cosplaying architecture.

The middle ground exists, and it's where most code that ages well lives: functions with one clear responsibility, explicit dependencies as parameters, small interfaces defined where they're consumed, and modules that do one thing.

That's SOLID. You don't have to call it that.

Top comments (1)

Collapse
 
mdenda profile image
Matías Denda

One thing I deliberately left out of the article to keep it short: the "SOLID in functional programming" debate. Curious where folks land on that — do you think the principles translate, or do they need a different framing entirely when there are no classes in sight?