Additive vs. Mutative vs. Destructive Code Changes (and Why AI Agents Love the Wrong One at 2:13AM)

There’s a particular kind of pain that only software developers know.

Not the “production is down” kind of pain.

Not the “we deployed on Friday” kind of pain.

No, I mean the slow creeping dread of pulling the latest changes, running the tests, and realizing the codebase has been “improved” in a way that feels like someone rearranged your entire kitchen… but left all the knives on the floor.

And lately, this pain has been supercharged by AI tooling. Cursor, Claude, Copilot, Gemini, ChatGPT-driven agents, whatever your poison is this week, all share a similar behavioral pattern:

They can produce a stunning amount of output at an impressive speed… while quietly reshaping your system into something that looks correct but behaves like an alien artifact from a parallel universe.

The reason is simple: AI agents don’t “change code” the way humans do. They don’t naturally respect boundaries unless you explicitly enforce them. They operate like an overly enthusiastic intern with root access and no fear of consequences. To understand why this happens, we need to talk about the different types of code changes, and how AI tooling tends to drift toward the most dangerous ones.

So let’s name the beasts.

The Four (Actually Six) Types of Code Changes

Continue reading “Additive vs. Mutative vs. Destructive Code Changes (and Why AI Agents Love the Wrong One at 2:13AM)”

Architects Among Us: The Many Shapes of Software Architecture

There’s a certain mystique that hangs around the title Software Architect. Some folks imagine a mythical being who levitates above the codebase, drawing circles and boxes on whiteboards, rarely touching a keyboard. Others see a burned-out senior developer who couldn’t let go of control, so they were promoted sideways. Both caricatures exist. Neither tells the whole story.

After years of designing systems, writing code, and watching teams rise and collapse under architectural decisions, I’ve realized that software architecture is less about diagrams and more about bridging people, systems, and time. It’s about building something that won’t rot under its own weight while still shipping what the business needs today.

The Job, at Its Core

A software architect’s real job is to balance competing forces. You’re thinking about performance, maintainability, developer experience, delivery velocity, and of course, cost. You deal in tradeoffs, not absolutes.

At its simplest, the role centers on three things:

  1. Defining the structure and intent of the system and what we’re building, and how it fits together.
  2. Guarding the integrity of that structure as it evolves.
  3. Communicating decisions clearly across the organization, from executives to developers.

It sounds straightforward until you’re six months into a project, two refactors deep, and someone in leadership just promised a feature that blows your architecture wide open.

The Many Flavors of Architect

There isn’t just one “Software Architect.” There are variations, each shaped by how close they stay to the code and how they interact with people and process.

The Ivory Tower Architect

This type rarely writes code and speaks in abstractions so detached from reality that teams can’t implement them without rewriting half the stack. The intentions are usually good, high-level vision, strong conceptual models,but the execution falls apart because they’re disconnected from how things actually get built and deployed. You’ll find them making PowerPoints, not pull requests.

The Technical Architect

This one’s hands-on. They know the quirks of the stack, the CI/CD pipeline, the caching edge cases, and which query in the database keeps locking under load. They’re the first to prototype, the last to stop tweaking configurations. They live closer to engineering and sometimes lose sight of business priorities, but when they’re balanced, they become the backbone of any effective engineering effort.

The Practice Architect

This is where the role expands beyond a single project. A practice architect doesn’t just design systems, they design how systems get designed. They establish frameworks, decision records, and architectural practices that teams can use without waiting for top-down approval. Their deliverables aren’t just diagrams, but processes: decision logs, architecture review boards, documentation standards, and communication channels that make architectural intent stick beyond a single sprint.

They drive consistency across multiple teams, creating the connective tissue that enables autonomy without chaos. Common patterns, shared libraries, CI/CD foundations, and standard observability practices are all part of their toolkit. The practice architect creates the scaffolding that keeps an engineering organization from splintering apart.

The Work: Business, Technical, and Solution Specific

A solid architect has to be fluent in three languages: business, technical, and solution. Each speaks a different truth.

Business Work

Here’s where the architect translates intent into implications. Understanding the revenue model, customer expectations, regulatory constraints, and delivery pressure is part of the job. You turn “real-time analytics” into “streaming data ingestion with 99.9% uptime and a sub-500ms query response.” That translation is the work.

Technical Work

This is where you live in the code and infrastructure. Choosing frameworks, defining service boundaries, selecting protocols, and ensuring scalability and resiliency are the bread and butter. You make sure the system can evolve. This is where non-functional requirements stop being buzzwords and start being design constraints you engineer around.

Solution Work

This is synthesis. You take the business intent and technical constraints, and build something that delivers both. It’s part art, part engineering, and entirely iterative. It’s also where you wrestle with the mess; the legacy code, the politics, and the reality that greenfield never stays green for long. Good solution architecture isn’t about the cleanest design, it’s about the right design for right now.

Working Across the Organizational Web

The real power of an architect isn’t in drawing boxes, it’s in connecting people who live in different ones. Architecture is as much a social system as it is a technical one.

A capable architect spends just as much time aligning teams as writing code or reviewing it. They operate in the seams of the organization: the handoff between Product and Engineering, where DevOps meets Security, where Marketing’s promises collide with technical reality, and where Support feels the fallout from design shortcuts. This bridging work is fundamental. It’s not optional, and it’s not “soft skills.” It’s the backbone of how complex systems actually get delivered.

Every senior technical role shares this responsibility. Staff, Principal, or Architect it doesn’t matter what your title says, your success depends on your ability to bridge silos. As a Principal Engineer, you’re often spanning horizontally across product lines and technical domains, ensuring cohesion between efforts. As an Architect, you’re bridging vertically and horizontally while translating business strategy into technical execution and back again. Both require deep context, credibility, and communication.

You talk to Product about tradeoffs and timelines. To Technical Product Owners about system boundaries and risk mitigation. To Developer Relations about usability and external developer experience. To Marketing about aligning technical capabilities with how the product is positioned. To Support and Operations about observability, resilience, and making sure the system doesn’t implode at 2AM. You’re the connective tissue between worlds that don’t naturally interact.

That’s what makes this level of engineering different. It’s not about writing more code. It’s about broadening your influence about seeing how design decisions ripple through people, processes, and systems. You learn to speak the language of each group without losing your technical grounding. Product won’t listen if you don’t understand deadlines. Engineers won’t respect you if you can’t code. Executives won’t care if you can’t tie architecture to revenue or risk. You have to earn trust in every direction.

The Reality Check

A software architect is part diplomat, part engineer, part historian. You carry institutional memory, technical rationale, and enough humility to admit when a “perfect design” isn’t the right one today. You have to stay close enough to the implementation to feel its pain, but far enough from the weeds to see the horizon. The balance is strategy informed by code, and code informed by strategy.

To be truly effective as an architect you don’t live in an ivory tower. You live in the intersection where technical ambition meets human limitation. And if you’re doing it right, you build systems that last just long enough for someone else to rewrite them better. 🤙🏻

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)”