A Reflection on SOLID: Decades of Code, Principles, and a Changing Future

After decades of building, breaking, refactoring, and rebuilding systems, from scrappy startups to enterprise labyrinths, I’ve seen a lot of patterns come and go.
But few have stuck around as stubbornly as the SOLID principles.

They’re like the veteran developers of software philosophy: reliable, experienced, and still showing up to standups long after everyone else has moved on to the latest framework or architecture fad. For years, teams I’ve been on, and many I’ve led, have leaned heavily on SOLID as the bedrock of maintainable software design. For the most part, it’s served us well.

Still, as the craft of development shifts into an AI-augmented era, I can’t help but wonder: is SOLID still as solid as it once was?

The Foundation We Built On

Let’s run through the familiar set – but not as definitions you could pull from Wikipedia. Let’s talk about what they actually meant in practice.

Single Responsibility Principle
This was the sanity rule. Keep your classes (or other similar code elements) from doing too much. If it has more than one reason to change, it’ll eventually collapse under its own weight. I learned this one early, often the hard way, cleaning up classes that had taken on the personality of their developer, a little of everything, in one chaotic pile.

Open/Closed Principle
The guiding star of extensibility: extend without modifying. In theory, that meant safety from regression and in many cases if done well it would help with regression and ongoing feature development. In reality, it meant endless debates about whether you were “violating OCP” every time you opened a file to fix something.

Liskov Substitution Principle
The quiet workhorse. Inheritance should make sense. If your subclass breaks expectations, you’re not using polymorphism, you’re just lying to your codebase.

Interface Segregation Principle
This was the rebellion against “god interfaces.” The countless times I’ve seen an IThingManager with thirty methods, half of which every implementation ignores. ISP was the call to split those monsters into sane, digestible contracts.

Dependency Inversion Principle
The rise of abstractions over implementations. This principle was both a blessing and a curse, the birthplace of dependency injection frameworks and inversion-of-control containers that we alternately loved and cursed. It gave structure but also a new layer of complexity.

When SOLID Worked

Over the years, I’ve seen SOLID absolutely save teams, including mine, from coding disasters. For example when a project scaled from three developers to thirty, SOLID acted as the stabilizing force. It created a shared language around what “good code” meant.

It kept monoliths from turning to spaghetti. It made refactoring survivable. It let teams ship features faster without worrying that a small change in the billing module would trigger a cascade failure in authentication.

In essence, SOLID made code cooperative. It taught us to think in modules, contracts, and boundaries. Those were lessons worth keeping.

When SOLID Became a Problem

But for every clean, modular, testable success story, there’s an over-engineered mess hiding behind the same banner.
I’ve walked into codebases where every noun in the business domain had an interface, an abstract class, and two decorators, none of which did anything meaningful. All in the name of being “SOLID.”

Here’s where it tends to go wrong:

  • Too Many Abstractions. “Open for extension” became “never touch anything again.” Layers of indirection were added to prevent change, not to enable it.
  • Framework Fetishism. Dependency injection containers got treated like religion. If you weren’t injecting it, mocking it, or wrapping it in a factory, were you even a developer?
  • Premature Architecture. Entire hierarchies were built for features that never came. Extensibility for the sake of hypotheticals.
  • Cognitive Overhead. Code so “modular” that new developers spent a week just tracing through interfaces before they found the line that actually did something.

SOLID was meant to reduce complexity, but taken too far, it became its own source of it.

The Shift: SOLID in the Age of AI-Generated Code

Now we’re standing at a strange crossroads.
AI-assisted development has changed the cost equation of software. Code is no longer expensive to write, it’s expensive to understand.

That changes everything.

When an AI can rewrite, refactor, or regenerate entire systems on command, the reasons we relied on SOLID begin to shift. SOLID was about human maintainability, protecting code from the chaos of change over time. But if AI can refactor in seconds what once took hours, do we still need all that protective architecture?

Maybe not. Or maybe, it needs reinterpretation.

AI doesn’t care if your class violates the Single Responsibility Principle; it can just regenerate it into smaller, purpose-built components when needed. But for humans reading, debugging, or reasoning about the system, SOLID still matters. It’s how we think about structure, even if we don’t handcraft it anymore.

The next evolution might be AI-native SOLID, principles guiding how AI systems generate and organize code for clarity, composability, and self-repair.

What Remains Solid?

In the end, SOLID wasn’t just about code. It was about discipline. About having a mental model for systems that grow beyond a single developer’s head.

But as AI tooling takes over more of the mechanical parts of design, the real question becomes:

Are we still designing for humans to understand the system or for systems to understand themselves?

That’s the next frontier.
And maybe, just maybe, it’s time to redefine what “SOLID” software design means in this new era.

VS Code & Copilot: The Chat-First Spec Definition Method

My initial review of CoPilot and getting started is available here.

(What it does well – and why it’s not magic, but almost!)

Let me be clear: the Copilot Chat feature in VS Code can feel like a miracle until it’s not. When it’s working, you fire off a multi-line prompt defining what you want: “Build a function for X, validate Y, return Z…” and boom VS Code’s inline chat generates a draft that is scary good.

  • What actually wins: It interprets your specification in context – your open file, project imports, naming conventions – and spits out runnable sample code. That’s not trivial; reputable models often lose context threading. Here, the chat lives in your editor, not detached, and that nuance matters.
    It’s like sketching the spec in natural language, then having VS Code autocomplete not just code but entire behavior.
  • What you have to still do: Take a breath, a le sigh, and read it. Always. Control flow, edge cases, off-by-one errors Copilot doesn’t care. Security? Data leakage? All on you. Copilot doesn’t own the logic; it just stitches together patterns it’s seen. You own the correctness.
  • Trick that matters: Iterate. Ask follow-up: “Okay, now handle invalid inputs by throwing InvalidArgumentException,” or “Refactor this to async/await.” Having a chat continuum in the editor is powerful but don’t forget it’s your spec, not the AI’s.

Technique 2: Prompt With Skeleton First

Skip blindly describing behavior. Instead, scaffold it:

// Function: validateUserInput
// Takes { name: string, age: number }
// Returns { valid: boolean, errors: string[] }
// Edge cases: missing name, non-numeric age

function validateUserInput(input) {
  // ...
}

Then let Copilot fill in the body. Why this rocks:

  • You’re giving structure; types, return shapes, edge conditions.
  • The code auto-generated fits into your skeleton, adhering to your naming, your data model.
  • You retain control over boundaries, types, and structure even before Copilot chimes in.

Downside? If your skeleton is misleading or incomplete, Copilot will “fill in” confidently, in code that compiles but does the wrong thing. Again, your code review has to rule.

Technique 3: In-Context Refactoring Conversations (AKA “Let me fix your mess, Copilot”)

Ever accepted a Copilot suggestion, then hated it? Instead of discarding, turn on Copilot Chat:

  • Ask it: “Refactor this to reduce nesting and improve readability,” or “Convert this to use .reduce() instead of .forEach().”
  • Watch it rewrite within the same context not tangential code thrown at you.

That’s one of its massive values – context-aware surgical refactoring – not blanket “clean this up” that ends in a different variable naming scheme or method order from your repo.

The catch: refactor prompts depend on Copilot’s parsing of your style. If your code is sloppy, it’s going to be sloppily refactored. So yes you still have to keep code clean, comment clearly, and limit complexity. Copilot is the editor version of duct tape not a refactor wizard.

The Brutal Truth

  • VS Code + Copilot isn’t a magical co-developer. It’s a smart auto-completer with chat, living in your IDE, context-aware but utterly obedient to your prompts.
  • The trick is not the AI it’s how you lead it. The better your spec, skeleton, or prompt, the better your code.
  • Your style skeptical, questioning, pragmatic fits perfectly. You don’t let it ride; you interrogate. And that’s exactly how it should be.

TL;DR Summary

TechniqueWhat WorksWhat Fails Without You
Chat-first specDetailed natural-language spec → meaningful codeNo spec clarity → garbage logic
Skeleton promptsProvides structure, types, expectationsBad skeleton = bad code, fast
In-editor refactoring chatContext-preserving improvementsMessy code → messy refactor

If you want more details on how you integrate Copilot into CI, or your personal prompt templates drop me the demand below, and I’ll tackle it head-on next time.

Go Concurrency Patterns(Resource Pooling Pattern)

Overview

The Resource Pooling pattern manages a pool of reusable resources (like database connections, HTTP clients, or file handles) to avoid the overhead of creating and destroying resources frequently. This pattern is essential for improving performance, managing resource limits, reducing overhead, and ensuring efficient resource utilization.

NOTE: For other posts on concurrency patterns, check out the index post to this series of concurrency patterns.

Implementation Details

Continue reading “Go Concurrency Patterns(Resource Pooling Pattern)”

Go Concurrency Patterns(Event Loop Pattern)

Overview

The Event Loop pattern processes events from multiple sources in a single thread using a central event loop. This pattern is essential for handling multiple event sources efficiently, managing I/O operations, building reactive systems, and coordinating multiple concurrent operations in a controlled manner.

NOTE: For other posts on concurrency patterns, check out the index post to this series of concurrency patterns.

Implementation Details

Continue reading “Go Concurrency Patterns(Event Loop Pattern)”

Go Concurrency Patterns(Singleflight Pattern)

Overview

The Singleflight pattern ensures that only one execution of a function is in-flight for a given key at a time. Duplicate callers wait for the result instead of executing the function again. This pattern is essential for preventing duplicate expensive operations, caching with concurrent access, API call deduplication, and database query optimization.

NOTE: For other posts on concurrency patterns, check out the index post to this series of concurrency patterns.

Implementation Details

Continue reading “Go Concurrency Patterns(Singleflight Pattern)”