Category Archives: Tech Debt

Reducing Technical Debt: Top Five Coding Resources

When I wrote my book about technical debt, I decided to concentrate more on personal mindset, team dynamics, and organizational influence. I did this because I thought that the coding aspects of paying technical debt were well-covered, and that I could just refer to them.

Even with these resources, I had still been on teams that struggled with more foundational questions of whether code was debt at all, whether it was worth paying, how to explain it to our management, and how to prioritize and plan projects to do something about it. That’s what my book is about.

If you want to read some free chapters of Swimming in Tech Debt, sign up here:

Here’s my list of resources that are helpful when you need practical steps for addressing tech debt that you have identified and prioritized.

These resources cover Seams, Refactoring, Substitutions, Accretions, and Tidyings. A full toolkit of techniques to tackle debt.

#1: Working Effectively with Legacy Code [ad] by Michael Feathers. In this book, Feathers defines legacy code as code without tests. So it makes sense that the bulk of the book is the description of techniques for adding tests to code that wasn’t designed to be testable. Feathers introduces the concept of a seam, which is code you add to a function that makes it possible to control what that function does from an external source. The key point is that, in production, its behavior is completely unchanged.

Here’s a trivial example to illustrate a seam. It will seem like overkill, but the technique is useful in real-world code. Given a function like this:

def calculate_sales_tax(amount, zip_code):
  rate = db_lookup_sale_tax_for_zip(zip_code)
  return amount * rate

In a codebase without tests or database mocking, functions like this are risky to change. The goal is to get it under test before we refactor it, so let’s do it with a seam to learn the idea.

The first step is to change it to this:

def calculate_sales_tax(amount, zip_code):
  rate = 0.08
  if not TESTING:
    rate = db_lookup_sale_tax_for_zip(zip_code)
  return amount * rate

By adding the if not TESTING: we’re accomplishing two things:

  1. We have not changed the production code
  2. We have made the code testable

So, the next two steps are to add tests, and then to refactor (which is safer because the code is under test). To see the utility of seams, you have to imagine doing this systematically in the giant spaghetti functions you find in legacy codebases. Also, in the book, Feathers shows more sophisticated ways to do this than just an if statement.

The beginning of the book sets up the seam concept, and the bulk of the book is examples of how to do this with more realistic code. This is the best “How To” book for tackling technical debt in legacy projects that I have read, and its existence meant that, in my book, I could concentrate more on the questions of “When to” pay technical debt and “Who should” do it.

The point of a seam is to make refactoring safe. So, the next book on this list is the seminal work on refactoring.

#2: Refactoring: Improving the Design of Existing Code [ad] by Martin Fowler. The main contribution of Refactoring was to make the term (“refactoring”) ubiquitous. Unfortunately, even Fowler had to admit, the term is often misused. Whenever you go to daily update, and someone mentions refactoring, you could be sure that they are not doing what Fowler described.

To Fowler, a refactoring is a tiny act. His book catalogs about a hundred types. Examples include Renaming a Variable and Extracting a Variable. Each of these takes seconds. The more complex ones might take minutes. Hardly something you’d avoid or mention. Refactoring is something you are meant to do all of the time. Assuming, of course, that your code is under test.

You can freely refactor in code that is under test because the point of a refactoring is to improve the code without changing the behavior. It’s quick because the verification is automatic. Without that, safe refactoring would take longer because you need to write tests first.

So, enabling refactoring might take time. Instead of saying that you are refactoring, a more correct thing to note in your updates is that you are automating tests on code that you need to change, so that you don’t introduce regressions. In addition, you are reducing the chance of problems being found in review or QA. This is something that does take time, but also has value. If you label this activity “refactoring”, you risk being asked not to do it.

In my book, I talk about refactoring along with the more expansive idea of Substitution, which comes from Barbara Liskov and was formalized in her paper with Jeanette Wing.

#3: A Behavioral Notion of Subtyping by Barbara Liskov and Jeanette Wing. This paper is about how to design classes in an object-oriented language so that they can be subtyped safely (and how to do that subtyping). It’s related to the ideas in design by contract and opposes the notion of allowing for breaking changes. Liskov substitution is about replacing components in systems where you cannot (or will not) update the rest of the codebase to accommodate them.

It does this through subtyping, but I have always interpreted it as applying to almost any change I make to a system, not just class inheritance. In Object-Oriented programming, subtyping is one way to add new functionality to a codebase, but in my experience, it’s not the main way. In my current work, I don’t use OO or subtyping much at all. But, I substitute code all of the time.

In the paper, the authors describe a way to subtype that is safe to deploy because it does not violate any assumption in the system. This way of thinking applies to any change you might make to code. If you can, it’s better to change the code in a way that doesn’t invalidate all of your other code. Or, if you must, you know that you aren’t “substituting” any more, and there’s a lot more work to do.

Like a refactoring, a substitution should not break tests, but unlike a refactoring, substitutions do not aim to preserve all behavior. In fact, the entire point of a substitution is to add functionality. The paper describes how to do it safely.

To sum it up, your new code will be substitutable for your old code if (in addition to any language syntax requirements): (1) it does not add required pre-conditions to the caller, (2) does not break promised post-conditions, (3) does not violate any invariants, and (4) does not mutate anything assumed to be immutable.

The paper is formal and might not the best entry-point. But, the Liskov Substitution wikipedia article describes it well, and if you want to read more about how I apply substitution to any code change, I cover that in Chapter 5 of my book.

Another great entry-point on this subject is the video Spec-ulation by Rich Hickey

#4: Spec-ulation (video) by Rich Hickey. In Spec-ulation, Rich Hickey talks about the concept of accretion and how to use it to grow software. Accretion is related to substitution and refactoring and is another way to approach changing code (to pay off tech debt, for example).

Rich makes two interesting (and related) points about accretion. (1) like Liskov, he defines safe actions such as “doing more” and “requiring less”, which map to pre- and post-conditions (2) additionally, he argues that Clojure’s syntax makes many more accretions (i.e. substitutions) non-breaking.

In her definition of substitution, Liskov states that all compile-time, syntax, or language level errors introduced by new code are (by definition) non-substitutable. The paper is about “behavior” breaking substitution that is syntactically correct, but will fail at runtime.

For example, in C, if you add a new parameter to a function, that function is not substitutable because all callers need to be changed. In languages like Clojure, adding a parameter is not a syntax error because there is a default and callers don’t need to change. Rich’s talk is about the many syntax affordances in Clojure that allow future accretion (and is a warning about using new tools to subvert them).

He argues that language features like strong type checking, which offer compile-time correctness, limit software growth through accretion because changes that would be non-breaking (and safe) cause syntax errors. This can work well if you control all of the code and can update it. But it causes problems in public code libraries. Clients cannot upgrade their dependencies to get bugfixes and new features if that means their build will be broken for code that was working fine.

The takeaway here is to look at your language’s features and see some of these affordances as a way to pay off tech debt safely. For example: one, often overlooked, feature is namespaces. Using them, you can keep old and new (replacement) code running side-by-side until you are sure it’s ok to remove the old one.

I learned so much about growing code from Kent Beck in eXtreme Programming: Explained, but a more recent book from Beck is more specific to technical debt.

#5 Tidy First?: A Personal Exercise in Empirical Software Design [ad] by Kent Beck. This book builds on Beck’s previous books and the ones inspired by them (like Refactoring). In it, he introduces the idea of a tidying, which like a refactoring, aims to improve code without changing behavior. It is also meant as something to be done all of the time and to be quick and safe (because of tests).

Tidyings, if possible, are even smaller than refactorings. They are more about making code more readable now rather than making it easier to change (which refactorings do). Tidyings are things you can do to help you be a more active reader of code. For example, getting the reading order right and using whitespace better to chunk the code. Kent describes it as a way to get back into the original spirit of refactoring (tiny, safe changes that don’t alter behavior).

With seams, refactorings, substitutions, accretions, and tidyings, you can do a lot to improve a codebase and make it easier to change. This reduction of resistance to change is what I mean when I say to pay tech debt, and these resources detail the main ways I do it.

Re-Onboarding Via Tech Debt Payments

I just opened a project I haven’t looked at in a few weeks because “holidays”. First thing I did was run tests, which didn’t run because I didn’t run them correctly. I looked at the README and it had no documentation for running tests.

In my book, Swimming in Tech Debt, I talk about this in the opening of “Chapter 8: Start with Tech Debt”. You can read this sample chapter and others by signing up to my list:

It opens:

You know the feeling. You sit down at your computer, ready to work on a feature story that you think will be fun. You sort of know what to do, and you know the area of code you need to change. You’re confident in your estimate that you can get it done today, and you’re looking forward to doing it.

You bring up the file, start reading … and then your heart sinks. “I don’t get how this works” or “this looks risky to change,” you think. You worry that if you make the changes that you think will work, you’ll break something else.

What you are feeling is resistance, which triggers you to procrastinate. You might do something semi-productive, like reading more code. Or you might ask for help (which is fine, but now you’ll need to wait). Maybe you reflexively go check Slack or email. Or worse, you might be so frustrated that you seek out an even less productive distraction.

The chapter is about immediately addressing this debt because you know it is affecting your productivity. It’s essentially free to do something now rather than working with the resistance.

So, following my own advice:

  1. I added text to the README explaining the project dev environment and how to run tests and get coverage data.
  2. Seeing the coverage data, I saw a file with 0 coverage and immediately prompted Copilot to write a test for one of the functions in it.

And that was enough to get warmed up to start doing what I was originally trying to do.

Vibe Coding vs. Vibe Engineering

I try to use Vibe Coding in Andrej Karpathy’s original sense:

There’s a new kind of coding I call “vibe coding”, where you fully give in to the vibes, embrace exponentials, and forget that the code even exists.

Which makes it hard to describe what I do, which is not that. I have been calling it AI-Assisted Programming, but that’s too long. Simon Willison proposed Vibe Engineering:

I feel like vibe coding is pretty well established now as covering the fast, loose and irresponsible way of building software with AI—entirely prompt-driven, and with no attention paid to how the code actually works. This leaves us with a terminology gap: what should we call the other end of the spectrum, where seasoned professionals accelerate their work with LLMs while staying proudly and confidently accountable for the software they produce?

I propose we call this vibe engineering, with my tongue only partially in my cheek.

He wrote this in October, but it only started to sink in with me recently when he wrote about JustHTML and how it was created. Read the author, Emil Stenström’s, account of how he wrote it with coding agents. This is not vibe coding. He is very much in the loop. I think his method will produce well-architected code with minimal tech debt. Like I said in my book: “The amount of tech debt the AI introduces into my project is up to me.” I think this is true for Emil too.

My personal workflow is to go commit by commit, because it’s the amount of code I can review. But, I see the benefit of Emil’s approach and will try it soon.

Dependency Maintenance vs. Supply Chain Attacks

I am assuming that you basically know what a supply chain attack is, but briefly, it’s when the code you install as a dependency in your development project contains malware. Unfortunately, all dependencies are code, and this code is usually run at a high privilege without needing to be signed.

The main thing it will try to do is grab keys and secrets from your .env files or environment variables and exfiltrate them. Some are targeted at blockchain developers and will try to steal their coins.

This is not a comprehensive guide. I am documenting my decisions based on my needs.

Like William Woodruff, I agree that We Should All Be Using Dependency Cooldowns. TL;DR is in the title. Essentially, never install a dependency that isn’t at least a few days old. The downside is defense against 0-day security fixes. If this is an issue, you could take the time to investigate and adopt the fix with an override.

The other broad advice with little downside is to not allow install scripts to run. You might still install malware, but if you let the install scripts run, they own you immediately. But since you are likely about to run the code inside your project, it’s not much protection. I do it anyway. The downside is when a dependency needs its post-install script to work. I used can-i-ignore-scripts to check for this issue when I used npm.

Ultimately, though, I have decided to leave the npm ecosystem and stop using node and React. Other ecosystems can have supply chain problems, but npm is having them on a regular basis because they are a prime target, and their practices have not scaled enough to deal with this.

I have also left Cursor and gone back to VSCode because Cursor’s fork cannot install the latest version of VSCode extensions. Extensions are also part of the supply chain and can be either malware or a hacking vector, so not being able to update them is not an option for me.

My next decision was to build a dedicated machine for software development. This machine does not have my personal data or information on it. It is not logged into any personal service (like my email). I have not yet dockerized all of my dev environments on it, but that’s a likely next step.

I also limit my dependencies. Another benefit of leaving the JS ecosystem is that Python isn’t as reliant on so many tiny dependencies. I was shocked at how many dependencies React, TypeScript and node/Express installed (I counted 10s of thousands of files in node_modules), and this is before you have written one line of application code. I like the batteries-included ethos of Django and Python. Most of what I need is built-in.

I have written a lot about dependencies and how it’s tech debt the moment you install it.

My final defense against supply chain problems is to have a regular dependency updating policy. Of course, this needs to be done with a cooldown, but my main reason to do it is because ignoring dependencies makes it very hard to do something about problems in the future. The more out of date you are, the harder everything is. Regular updating will also remind you of how bad it is to have dependencies.

To make this palatable, I timebox it. It really should take less than an hour for my project. Even at Trello, it only took a few hours to update the iOS project, which we did every three weeks. You also need extensive, automated test suites and time to test manually.

If updating takes longer for some reason, then the dependency that is causing this is now suspect. I will probably plan to remove it. If I need it (like Django), then I consider this a dry-run for a project I need to plan.

Moats and Fast Follow For Vibe Coded Projects

I wrote about how, at Atalasoft, I told my engineers to Be Happy When It’s Hard. Be Worried When It’s Easy. We competed against open-source and in-house solutions. When we found valuable problems that were hard to solve, I was relieved. The same is true for vibe coded solutions.

If you can create a valuable app in two weeks, then so could a competitor. If your secret sauce is your idea, then that’s hard to protect if you want people to use your app. We don’t even know if AI generated code is copyrightable, so it’s very unlikely to be patentable (i.e. inventors must be humans).

Here are three things you could do:

  1. Keep building on the idea – right now, someone following you has the benefit of seeing your solution and feeding that to the AI. So, it helps if you can keep building on the idea and hope they can’t keep up. If you do the minimum, the bar is too low.
  2. Build on secret data – once you have a working system, the biggest moat you have is the data inside the system. AI can’t see that or reproduce it from scratch. Build new (valuable) features that require secret data to work. This doesn’t need to be used as training data. This is like a network effect, but more direct and long-lasting.
  3. Use your unique advantages – If your app is a simple UI on CRUD operations, then it can be reproduced by anyone. But, let’s say, you have personal branding in a space. Can you make an app that extends on it? Do you have access to hard-to-win customers? A mailing list, subscribers, etc? Fast-followers might be able to recreate your software but your audience won’t care if they trust only you.

Of these, I am relying mostly on the last one. The software I am working on is an extension of Swimming in Tech Debt. It takes the spreadsheet that I share in Part 3 and builds on it with better visualizations than the built-in ones. Someone could clone this, I guess, but probably they would need to reference my book in order to explain it. I am indifferent to whose software they use if this is true.

It’s not Debt if You Don’t Care About the User

I recently read Are consumers just tech debt to Microsoft? by Birchtree, where they say:

Microsoft just does not feel like a consumer tech company at all anymore. Yes, they have always been much more corporate than the likes of Apple or Google, but it really shows in the last few years as they seem to only have energy for AI and web services. If you are not a customer who is a major business or a developer creating the next AI-powered app, Microsoft does not seem to care about you.

Their thesis is that Microsoft’s share of the consumer market will plummet because the consumer is tech debt to them. I think of the user as a facet of tech debt, not the debt itself.

In Swimming in Tech Debt, I present eight questions you should answer about tech debt. One of them, called “Regressions”, asks how likely it will be that you will break working code for users that you care about. The more you might, the more, I believe, that you should not touch this code (or be very careful with it).

But, if you don’t care about the users, or they don’t care about the features the indebted code provides, then it’s likely that you can just rewrite it with impunity. You can change it without risk. You might be able to delete it. If so, it’s hardly a debt.

If you do value a market and change code radically, the consequences can be fatal (see Sonos). But if you don’t, then doing the minimum is rational.

Workshop: Eight Questions to Ask About Your Tech Debt

In Part 3 of my book, Swimming in Tech Debt, I write about how teams should plan projects to address larger technical debt issues. The bulk of the chapters in the section explain how to manage a tech debt backlog.

Drawing on the practices of product managers and how they manage feature backlogs, I propose a scoring system to drive the discussion.

The scoring system breaks down the costs and benefits of paying debt (or not paying it) and gives you a way to compare items to each other. It starts with this diagram:

Diagram showing Pay and Stay Forces

The benefits of paying debt and the costs of not paying (staying) drive the Pay Force. Inversely, there are benefits to staying and costs to paying that indicate you should leave the debt alone. These eight dimensions are scored by answering a related question:

  1. Visibility: If this debt were paid, how visible would it be outside of engineering? 
  2. Misalignment: If this debt were paid, how much more would our code match our engineering values?
  3. Size: If we knew exactly what to do and there were no coding unknowns at all, how long would the tech debt fix take?
  4. Difficulty: What is the risk that work on the debt takes longer than represented in the Size score because we won’t know how to do it?
  5. Volatility: How likely is the code to need changes in the near future because of new planned features or high-priority bugs?
  6. Resistance: How hard is it to change this code if we don’t pay the debt?
  7. Regression: How bad would it be if we introduced new bugs in this code when we try to fix its tech debt?
  8. Uncertainty: How sure are we that our tech debt fix will deliver the developer productivity benefits we expect?

If you have bought my book and would like me to talk to your team about this process, get in touch. It would be a 45-minute presentation with 15 minutes for Q&A.

In the presentation, I score 3 backlog items from my career and then show how the scoring drives the decision making of what to do. I encourage you to record it and then go through the presentation with a couple of examples from your backlog.

This workshop is free. Write me on LinkedIn or through my contact page.

After taking the workshop, reach out if you would like me to facilitate your technical debt backlog planning sessions. The book has agendas, scoring guides, and a catalog of score-driven debt remediation ideas, but I’m happy to tailor them to your needs.

Using Fuzzy Logic for Decision Making

In the 90’s, I read a book about fuzzy logic that would feel quaint now in our LLM-backed AI world. The hype wasn’t as big, but the claims were similar. Fuzzy logic would bring human-like products because it mapped to how humans thought.

Fuzzy Logic is relatively simple. The general idea is to replace True and False from Boolean logic with a real number between 1 (absolutely true) and 0 (absolutely false). We think of these values more like a probability of certainty.

Then, we define operations that map to AND, OR, and NOT. Generally, you’d want ones that act like their Boolean versions for the absolute cases, so that if you set your values to 1 and 0, the Fuzzy logic gates would act Boolean. You often see min(x, y) for AND and max(x, y) for OR (which behave this way). The NOT operator is just: fuzzy_not(x) => 1.0 - x.

If you want to see a game built with this logic, I wrote an article on fuzzy logic for Smashing Magazine a few years ago that showed how to do this with iOS’s fuzzy logic libraries in GameplayKit.

I thought of this today because I’m building a tool to help with decision making about technical debt, and I’m skeptical about LLM’s because I’m worried about their non-determinism. I think they’ll be fine, but this problem is actually simpler.

Here’s an example. In my book I present this diagram:

Diagram showing Pay and Stay Forces

The basic idea is to score each of those items and then use those scores to make a plan (Sign up to get emails about how to score and use these forces for tech debt).

For example, one rule in my book is that if a tech debt item has high visibility (i.e. customers value it), but is low in the other forces that indicate it should be paid (i.e. low volatility, resistance, and misalignment), but has some force indicating that it should not be paid (i.e. any of the stay forces), then this might just be a regular feature request and not really tech debt. The plan should be to put it on the regular feature backlog for your PM to decide about.

A boolean logic version of this could be:

is_feature = visible && !misaligned && !volatile && !resistant && 
              (regressions || big_size || difficult || uncertain)

But if you did this, you have to pick some threshold for each value. For example, on a scale of 0-5, a visible tech debt item be one with a 4 or 5. But, that’s not exactly right because even an item scored as a 3 for visibility should be treated this way depending on the specific scores it got in the other values. You could definitely write a more complex logical expression that took this all into account, but it would hard to understand and tune.

This is where fuzzy logic (or some kind of probabilistic approach works well). Unlike LLMs though, this approach is deterministic, which allows for easier testing and tuning (not to mention, it’s free).

To do it, you replace the operators with their fuzzy equivalents and normalize the scores on a 0.0-1.0 scale. In the end, instead of is_feature, you more get a probability of whether this recommendation is appropriate. If you build up a rules engine with a lot of these, you could use the probability to sort the responses.

Fuzzy logic also allows you to play with the normalization and gates to accentuate some of the values over others (for tuning). You could do this with thresholds in the boolean version, but with fuzzy logic you end up with simpler code and smoother response curves.

Dev Stack 2025, Part VII: Sqlite

This is part of a series describing how I am changing my entire stack for developing web applications. My choices are driven by security and simplicity.

Since Django uses an ORM, switching between databases is relatively easy. I usually pick MySQL, but I’m going to see how far I can get with Sqlite.

The project I am working on is to make a better version of the tech debt spreadsheet that I share in my book (sign up for the email list to get a link and a guide for using it). The app is very likely to be open-source and to start out as something you host yourself. So, I think Sqlite will be fine, but if it ever gets to the point where it won’t work, then switching to MySQL or Postgres shouldn’t be that hard. My DB needs are simple and well within the Django ORM’s capabilities.

Even if I decide to host a version, I might decide on a DB per tenant model, which might be ok for Sqlite. Another possibility is that it would be something in the Jira Marketplace, and in that case, I’d have to rewrite the backend to use Jira for storage, but that wouldn’t be that bad because (given the Jira data-model) I only need to add some custom fields to an issue. Most of the app at that point would be the visualizations and an expert system.

One nice thing about Sqlite is that it’s trivial to host. It’s just a few files (with WAL mode). It’s also trivial to run unit-tests against during development. You can do it in-memory, which is what Django testing does by default. I can also run those test suites against more powerful databases to make sure everything works with them too.

One portability issue is that if I get used to running against Sqlite, I will probably not notice performance issues. Since Sqlite is just some local files, it’s incredibly fast. You can feel free to do lots of little queries to service a request and not notice any latency issues. The same style over a network, potentially to a different datacenter, won’t work as well.

But I have seen enough evidence of production SaaS products using Sqlite, that I think I can get to hundreds of teams without worrying too much. I would love to have a performance problem at that point.

In my book, I talk about how technical debt is the result of making correct decisions and then having wild success (that invalidate those choices). I don’t like calling these decisions “shortcuts” because that word is used a pejorative in this context. Instead, I argue that planning for the future might have prevented the success. If this project is successful, it’s likely that Sqlite won’t be part of it any more, but right now it’s enabling me to get to first version, and that’s good enough.

Changing my Dev Stack (2025), Part I: Simplify, Simplify

My life used to be easy. I was an iOS developer from 2014-2021. To do that, I just needed to know Objective-C and then Swift. Apple provided a default way to make UI and it was fine.

But, in 2021, when I went independent, I decided to abandon iOS and move to a React/Node stack for web applications (that I wanted to be SPA). I chose React/ReactNative. It was fine, but I have to move on.

The main reason is the complexity. For my application, which is simple, there are an insane amount of dependencies (which are immediate tech debt, IMO). Hello World in Typescript/Node/React will put 36,000 files (at last count) in node_modules. This reality has become a prime target for hackers who are using this as a vector for supply chain attacks. It’s clear that the node community is not prepared for this, so I have to go.

This is a major shift for me, so I am rethinking everything. This was my criteria:

  1. Minimal dependencies
  2. No build for JS or CSS
  3. Security protection against dependencies
  4. Bonus if I already know how to do it

The first, and easiest decision, was that I was going to move all development from my Mac to Linux. I’ll talk about this tomorrow in Part II.