Make things that are easy to delete

23 hours ago 1

Christian Jensen

Ever tried to clean out an old closet? If the boxes are neatly labelled and packed with similar items, it’s easy to toss the whole thing when you no longer need it. But if those boxes are full of tangled cables, stacks of paperbacks and winter jackets from ten years ago? That cleanup turns into a weekend-long “project”.

That same idea applies to software.

Most of what you build will eventually be deleted. Not immediately. Sometimes not for years. But it will be. That API endpoint? That queuing system? That big-bet feature no one used? Eventually they go stale, get replaced, or become maintenance burdens. The real trick isn’t just building something that works. It’s building it in a way that’s easy to delete later.

Before we dive deeper, let’s clarify what makes a system deletion-friendly:

  • Loose coupling — components interact through well-defined interfaces, not hidden side effects.
  • Clear ownership — every system, module and document has an owner.
  • Bounded contexts — group functionality together with strong cohesion and clear boundaries.
  • Minimal blast radius — failure or deletion of one piece shouldn’t ripple across your whole system.
  • No clever, hidden dependencies — clever code is often fragile code. Be boring. Be obvious.

The best time to think about deletion is before you even start writing code. Sounds backward, but it’s not. It means being honest with yourself that this thing might not last forever. In fact, most things won’t. And that’s okay.

You might be launching an experiment behind a feature flag. You might be writing the first version of a new money-making feature. You might be working fast and cutting corners, fully aware you’ll optimize later. These are the perfect times to ask: how can I make this easy to delete?

  1. Limit your dependencies. Especially upstream ones. If you need to delete something and it’s tied into five other systems, you’re going to regret it.
  2. Interface everything. Not just across the network. I mean internally, too. When you hear “API,” don’t just think REST or gRPC. Think good old-fashioned code boundaries: clearly defined data structures and a well-thought-out interface. Something you can stub, override, or rip out.
  3. Structure your files like you expect to throw them away. Group them by feature. Keep related pieces close together. Keep unrelated ones apart. This makes it easier to surgically remove something later.
  4. Use strategy patterns or plugin architectures. Anything that lets you swap behavior without rewriting your core logic helps.
  5. Wrap anything you might vary behind a function. It’s often better to return an implementation from a getter than reference a class directly.

Code Structure

  • Keep files small and focused
  • Avoid tight coupling between classes
  • A feature flag should be deletable without chasing down six different call sites

Here’s a simple Python example:

from enum import StrEnum
from abc import ABC, abstractmethod

class ImageProcessorVersion(StrEnum):
v1 = "v1"
v2 = "v2"

class ImageProcessor(ABC):

@abstractmethod
def process(self, image: bytes) -> bytes:
pass

class ImageProcessorV1(ImageProcessor):
def process(self, image: bytes) -> bytes:
print("Processing with V1")
return image

class ImageProcessorV2(ImageProcessor):
def process(self, image: bytes) -> bytes:
print("Processing with V2")
return image[::-1]

def get_image_processor(version: ImageProcessorVersion = ImageProcessorVersion.v2) -> ImageProcessor:

# Dictionary mapping versions to their processor classes
processor_map = {
ImageProcessorVersion.v1: ImageProcessorV1,
ImageProcessorVersion.v2: ImageProcessorV2,
}

try:
processor_class = processor_map[version]
except KeyError:
raise ValueError("Unsupported processor version")

return processor_class()

This approach makes it easy to plug in new behavior, test in isolation and eventually sunset old logic without a massive rewrite. You can combine this with a feature flag system and get the ability to swap out functionality dynamically even.

Infrastructure

  • Can you nuke a service and nothing else breaks?
  • Use Terraform modules with clean state separation
  • Design infrastructure to be testable and ephemeral

Architecture

  • Microservices should be sunsettable without a monolith rewrite
  • Event-driven systems should allow independent evolution of producers and consumers

Processes

  • Cron jobs or batch jobs shouldn’t cause chaos when removed
  • Don’t let legacy tasks live on just because no one remembers why they exist

As your system grows, the way components communicate becomes a key design decision. It’s also one of the least visible forms of API — and it heavily impacts how easy things are to replace or remove later.

There are two common patterns:

  • Choreography is decentralized. Services listen for events and react. No single system is in charge. This pattern is often powered by tools like Kafka, NATS, or AWS SQS. It allows for loose coupling and flexibility, making services easier to plug in or delete. The downside? Debugging can get messy. Tracing the path of an event across systems takes discipline and tooling.
  • Orchestration is centralized. A controller dictates what happens and when. Tools like Temporal and AWS Step Functions provide durable workflows with retries and observability. Celery, by contrast, provides task distribution but lacks durability, making it more fragile. Orchestration makes flow control easier to understand and monitor — but introduces a single point of coordination.

Whichever pattern you choose, treat the coordination logic as just another module. You should be able to version it, refactor it, or delete it. Otherwise, it becomes a hidden liability.

Use orchestration when you want control and visibility. Use choreography when you want flexibility and decoupling. In practice, many systems blend both. For deletion-friendly design, choreography has the edge — no one piece needs to know who else is listening.

Let’s tie this back to our earlier section on Choreography vs Orchestration. Those patterns are just higher-level manifestations of the API boundaries we design every day. In fact, they can be mapped directly to some of the structures below — with choreography often implemented using message buses and orchestration handled by workflow engines.

APIs aren’t just for external consumers. They show up at every layer of your system — and every boundary is a decision point for future deletion and flexibility.

  • Abstract Base Classes and Protocols — The simplest and fastest kind of API. No serialization, no networking. Everything stays in memory and moves by reference. These are excellent for strategy patterns and test doubles and they’re trivially easy to replace or extend.
  • Plugin Architectures — Dynamically loaded logic with well-defined interfaces. You can version or swap plugins independently. Great for optional features or system extensions.
  • Message Buses and Background Tasks (Choreography) — Asynchronous and loosely coupled. They’re harder to debug but allow pieces to evolve independently. A good boundary for long-running or slow operations that don’t require immediate response.
  • Workflows (Orchestration) — Systems like Temporal or Step Functions provide stateful orchestration across time and failures. They’re often built around a central orchestrator and act as a durable API for coordination. These can also be versioned and even deleted when modeled well.
  • Network APIs (REST, GraphQL, gRPC) — More expensive boundaries. You pay in latency, serialization and infrastructure. You often need to hit a database even after a slow network call to instantiate whatever is being called. But they’re valuable when teams or systems are distributed. Just be aware: these are the hardest APIs to change or remove, so design them with care.

As you move from in-process to cross-network APIs, your flexibility usually drops while your cost (time and as a side effect, money) goes up. That doesn’t mean avoid them — just know what you’re buying. And make sure every API, no matter how small, is designed with deletion in mind.

  • You move faster when systems are cleanly separable
  • New developers ramp up quicker
  • Refactoring becomes possible
  • Long-term ownership becomes cheaper

And the inverse is just as important:

If it’s hard to delete, it’s probably a liability.

This is not an exhaustive list of places to look but are some pretty easy to spot things that you can get into the habit of looking for.

Code reviews:

  • Could this be removed cleanly in the future?
  • Watch for logic that spans multiple layers or relies on global state.
  • Prefer code that uses interfaces or factory methods to make swapping implementations trivial.

Infrastructure:

  • Structure Terraform modules for independence. Each should be easy to destroy without impacting others.
  • Avoid shared state. Pass outputs downward when needed, never sideways.
  • Use naming and tagging that makes teardown obvious.

Documentation:

  • Add ownership and review dates to project documentation for a given area of code. Perhaps even make a ticket with an expiration date so that it gets on the work list.
  • Mark temporary systems or experimental features explicitly.
  • If a system has outdated or untouched docs, flag it for review or removal.

General practice:

  • Build systems as if no one will remember why they exist in six months.
  • Consider success as being able to delete something without anyone noticing.

Everything you build will rot. The trick is to treat your work as temporary, even when it feels permanent. Design for its replacement. Write like the next person won’t know what you meant. Assume you’ll delete it, because one day you will.

Deletion is a skill. It takes confidence, discipline and good design up front. But the best engineers I’ve worked with are the ones who knew what not to build and how to make what they did build disappear cleanly.

Read Entire Article