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.

26 for 26 in January

A few weeks ago, I made a list of small wins I am going to try to have in 2026. To help me be accountable to them, I am going to make an update post each month. Here’s what I have done so far:

  1. I found a place to get Nattō (my farmer’s market) and got a jar last week.
  2. I added A+ Content to the Amazon page for my book.
  3. I tried Amazon Ads (I’ll write a post next week about my thoughts on that).
  4. I cooked one new recipe from one of my vegan cookbooks last night (I think it will be worth doing again).
  5. I left 4 books in donation mini-libraries.
  6. I joined CrossFit again (it’s my best option for a 3rd Place).
  7. I made a reservation for a new (to me) restaurant in NYC.
  8. I went to a gathering of neighbors in my building and met a few new people.

And I am making progress on some of the other things on the list.

Joke Templates

Back in the nineties, I was interviewing someone and he mentioned the idea of joke templates. I can’t remember his example, but when I told my boss, he said, “Oh yeah, I love the one where someone says a number and then you multiply it by seven and say it’s that many in dog years.”

My favorite joke template is the two problems one. I think it was originally: “You have a problem and you think, ‘I know: I’ll use a regular expression’ — now you have two problems.”

I’m a sucker for any variant on this. I just posted this to LinkedIn:

You have a problem with your AI code generator undoing its own work when you add something new, so … “I know,“ you think, “I’ll add another LLM to check the code of the first one.“ Now you have two problems.

RegExes aren’t AI, but it felt that way sometimes since they are so good at what they are good at. But, just like LLMs, they suck at what they suck at. Generally, they are great at finding answers that have objective, verifiable truth. But, they are not good at knowing a secret fact. The key is to provide the facts and ability to verify, and then let the LLM iterate to the solution.

26 for 26

I heard this idea from Gretchen Rubin originally and was recently reminded of it by John August. The idea is to make a checklist of things to do in the upcoming year (i.e. 26 things to do in 2026). It’s another alternative to resolutions (see also: Yearly Themes).

Each item should be doable. To me, it’s an extension of Making Happiness a Priority and my aspirational goals to be Fuerte Y Suerte.

  1. Bench my weight
  2. Be able to do 10 pull-ups
  3. Make a meal with soy curls
  4. Find a place locally where I can get Fermented Bean Curd and Nattō
  5. Add A+ Content to the Amazon page for my book
  6. Try Amazon ads
  7. Go to an intermediate Spanish meetup
  8. Cook one new thing from each vegan cookbook I have
  9. Leave 10 books in the donation mini-libraries around town
  10. Hang pictures of my family in my guest room
  11. Go to a live musical
  12. Go to a NY Liberty game
  13. Go to at least one arena-sized concert
  14. Have a coffee or lunch with someone who lives in my building (see Improving My Social Connection Index)
  15. Join a gym that has a social component
  16. Host someone new for dinner at my place
  17. Try three new restaurants for dinner in Sarasota
  18. Try two new restaurants for dinner in NYC
  19. Go for a weekend away to a new place in Florida
  20. Attend a local political gathering
  21. Release something new to open-source
  22. Get App-o-Mat on current Django and back to being a live server (not static pages)
  23. Publish 10 articles on third-party sites
  24. Appear on 5 podcasts
  25. Publish 10 episodes of my podcast: Write While True Podcast
  26. Write 10 Amazon Book reviews for books I love

Blind Sketching

I want to sketch more, but it seems a little like a chore sometimes. For example, I used to go to life drawing sessions, but it’s a four hour commitment with travel. So, instead I’ve been doing more quick, blind sketches. In ink.

A blind sketch is a sketch you do by looking only at the subject and not your paper at all. It takes at most a couple of minutes for me.

Here’s one I did from a hotel bed last week:

Blind sketch of some stuff on a shelf in a hotel

I like doing it in ink because there is no way to fix a mistake. You have to just move on.

Evening Pages

I have been doing Morning Pages on and off for a few years. If you are new to them, it’s the practice of writing handwritten, non-stop, stream of consciousness, non-edited text for three pages. It takes about 20-30 minutes once you get going. The benefit to me is that it proves that I can force myself to write. It trained me to push past whatever it was that held be back from writing more.

I started doing them again as an exercise in The First 13 Weeks of 2026. It’s part of how I am Making Happiness a Priority. It’s a small boost to the day when I’m finished.

But, for a couple of days this week, I didn’t find time in the morning and decided to do them at night instead—close to bedtime. In these sessions, I treated it more like a shutdown than a boot up.

In the morning, I search for ideas to write about later in the day. At night, though, I have been unloading my thoughts. The things that might occupy my mind as I try to drift to sleep get their final say.

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.

Making Happiness a Priority

My running coach, Holly Johnson, wrote a book a few years ago called How To Make Feeling Good Your Priority [ad] (my review). In it, she wrote about how she applies her running mindset to everyday life. Specifically, when feeling bad during a run (tired, hurt, etc), she would find a way to feel good right now. She’d seek one small adjustment that could make an immediate difference. Her aim was to stop a bad moment from becoming a bad day.

Related to that, I once got some advice to maintain a list of small things you could do to make you happy. They could be quick, or more involved. Perhaps some are situational and some could be done whenever. The idea is that you could use them to substitute out a bad habit or for a pick-me-up. Like, instead of doom scrolling, you listen to a song you like.

In that spirit, I have developed a list of small things that make me happy (or will make me feel like I had a good day). I am not trying to get all of them every day. They’re meant as a way to fill time with things that lift me up instead of doing things I would prefer to do less of. A lot of them are tied to my 2026 theme of Fuerte y Suerte, but not all.

  1. Work towards 10,000 steps
  2. Hang on a pull up bar (there’s one in my office)
  3. Sit in a deep squat for 5 minutes
  4. Go for a walk outside
  5. Work towards my daily protein goal
  6. Read a book
  7. Do Morning Pages
  8. Journal
  9. Selfie Video (5 min) to practice extemporaneous speaking
  10. Call, text, or meet with a friend
  11. Listen to Spanish (Podcast, Music)
  12. Fill a page with Spanish writing
  13. Code on a side project
  14. Tidy
  15. Meditate
  16. Quality time with my wife
  17. Listen to music
  18. Sketch

One nice thing is many of them can be combined. I can go for a walk outside, get steps, and listen to Spanish Music. Or go for walk outside with my wife. Read a book on the elliptical. I did my morning pages in Spanish yesterday. Many take just a few minutes. A hang is less than a minute right now. I can do it while waiting for a compile (jk, I use Python).

And, if I do want to just watch some YouTube, I can sit in a squat or find something in Spanish to watch. Or find some music videos (and dance).

If you are looking to find a way to break a bad habit, a list like this is useful to implement a substitution strategy. If January 1st makes you motivated to make change, then Use Motivation To Program Your Environment—make the list and put it somewhere you can see it.

2025 Blog Roundup

In 2025, I published 112 posts. Here’s what was on my mind.

My biggest accomplishment in 2025 was publishing my book, Swimming in Tech Debt. Here are some posts about the process:

If you want to read sample chapters from the book, sign up here:

I wrote a lot about code reviews.

I completely changed my dev stack from Node/React to Django/HTMX

These were some of my favorites

The most popular post from this year (mostly because of search hits) is Supernote Manta: Review at Eight Weeks. I updated it with my current thinking in How Digital Journaling is Better Than Paper.

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.