How to Write Simple Software

All you need is time
Fund this Blog

Every time I join a new company, I wonder how their code ends up becoming so complex. You’ll see an application that is supposed to do a simple task turn into a monstrosity that everyone is afraid to update. Writing simple software is often the goal, but somehow we go astray.

Tl;dr: Dedicate significant time to refinement *without* adding new features.

Not too long ago, I wrote about an application that I worked on in the past: "SQL Injection as a Service." It started as a humble tool to detect errors on a Set-top box. Its core job? Run specific diagnostic queries safely.

But then came the requests. "Can it also email the results?" "We need a web interface for non-techs!" "Make it pull configs from this other legacy system!" Feature requests arrived faster than we could breathe, let alone clean up. We slapped on email libraries, bolted on a web framework, and jury-rigged integrations with ancient APIs.

The focus was only on delivering the next feature. Security reviews? Refactoring? "We'll do it later." The result? A bloated, insecure behemoth where the original, simple diagnostic tool was buried under layers of incidental complexity and vulnerabilities. The complexity wasn't accidental; it was accumulated by neglect.

This prioritization of features over foundation is the kryptonite for simplicity. It creates a vicious cycle:

  1. The Quick Hack: You need a notification system fast. Instead of designing a clean pub/sub or event system, you scatter sendNotification() calls with hardcoded logic and // TODO: Refactor comments directly throughout your core logic. It works for now.
  2. The Next Feature: Before you can revisit those TODOs, a new request arrives: "Add user preferences for notification channels (SMS/Email)." Now you have to modify all those scattered sendNotification() calls, adding branching logic based on user settings. You're building complexity on top of a known weakness.
  3. The Entrenchment: Repeat this for months or years. Each new feature interacts with the existing, suboptimal structure, further entrenching it. That initial notification hack becomes a tangled web woven into the fabric of your application. Now, changing anything related to notifications feels risky and requires testing a dozen different paths. The cost of simplification seems prohibitive.

The Illusion: More project time automatically means adding more features.

The Reality: Truly simple, robust software requires dedicated time for subtraction, consolidation, and deep understanding, without the distraction of new scope.

I’m reminded of Steve Jobs’ insight:

“When you first start off trying to solve a problem, the first solutions you come up with are very complex, and most people stop there. But if you keep going, and live with the problem and peel more layers of the onion off, you can often times arrive at some very elegant and simple solutions. Most people just don’t put in the time or energy to get there.”

Jobs wasn't talking about adding features; he was talking about relentless refinement of the core.

How "No New Features" Time Leads to Simplicity:

This dedicated refinement time allows you to do the crucial work that feature pressure always pushes aside:

  1. Identify and Eliminate Duplication: You find three different places calculating a user's discount slightly differently. You create one authoritative, well-tested calculateDiscount() function. Benefit: Fix a bug once; changes are centralized; logic is clear.
  2. Uncover Hidden Abstractions: Those scattered notification calls? You realize they all stem from specific events (OrderPlaced, UserUpdated). You rip out the scattered calls and build a simple event bus. Now, handlers for OrderPlaced (which might send notifications, update analytics, and trigger inventory checks) are decoupled. Benefit: Adding a new action for an event (like triggering a loyalty reward) becomes trivial and isolated. The core logic is cleaner.
  3. Simplify Complex Conditionals: Nested if/else blocks handling edge cases from years of features? You refactor using guard clauses, polymorphism, or state patterns. Benefit: The main flow becomes readable; edge cases are handled explicitly and locally; future changes are less error-prone.
  4. Improve Naming and Structure: processData() becomes validateAndTransformIncomingOrder(). A "Util" class with unrelated methods is split into cohesive ValidationHelper and DateFormatting classes. Benefit: New developers (or future you) understand the code instantly. Discoverability improves.
  5. Strengthen Foundations: That direct database access scattered everywhere? You build simple, focused Data Access Objects (DAOs) or Repositories. Benefit: Changing database logic happens in one place; testing core logic without the DB becomes possible; security boundaries are clearer (goodbye, accidental SQL Injection risk!).
  6. Delete Dead Code & Deprecated Paths: You find entire features or code branches disabled by configuration flags or superseded years ago. You delete them. Benefit: Less code to maintain, understand, test, and secure. Reduced attack surface. Faster builds.

The Payoff: Why This Works

None of these things directly concern adding new features, yet this work helps you add features in the future. Here are the benefits:

Simple software isn't just written; it's refined. It requires the discipline to say, "We are not adding anything new this sprint/quarter. We are investing solely in making what we have simpler, clearer, and stronger." This isn't polishing, it's essential engineering work to avoid the inevitable descent into unmanageable complexity. To simplify your code, make time for it.


Comments

There are no comments added yet.

Let's hear your thoughts

For my eyes only