Francisco Tarulla

Francisco Tarulla

Hello Continuous Code Quality World!

May 22 2025

18 min

Hello and welcome to the second part of Hello Technical Debt World!

In this second part, we’ll focus on some tips, or best practices for handling our system’s immature code.

Disclaimer! I must mention that there is no silver bullet; only best practices that each development team applies (perhaps with some modifications) as they find most comfortable to develop software.

Now… let’s begin!

In the previous post, we reviewed the technical debt metaphor proposed by Ward Cunningham in 1992 to talk about the gap between what the code conveys and our current knowledge of the problem domain (and the actual reality). He also warns us of the risks of continuing to develop with immature code:

Although immature code may work fine and be completely acceptable to the customer, excess quantities will make a program unmasterable, leading to extreme specialization of programmers and finally an inflexible product […] Every minute spent on not-quite-right code counts as interest on that debt. Entire engineering organizations can be brought to a stand-still under the debt load of an unconsolidated implementation.

Knowing all this, we are certain that immature code, whether implemented during the learning cycle itself or consciously, must be fixed. Otherwise, it will continue to add complexity to our system by continuing to build it on misconceptions.

So, the first question that comes to mind is: if I can add immature code (and acquire technical debt)… how do I know if I have debt in my code?

Wait … is this immature code?!

Since we’re learning at the same time as we are developing our system, that’s likely the case. Let’s go back to the example in our previous post, and our YellowFlagCar concept. The code was already in production when the client told us the correct name for that concept, which is why the interaction with the person who knows about the problem’s domain is critical:

Immediate Feedback is key to the learning process, as it allows us to correct misconceptions before further reinforcing them. In this learning process, we must listen to someone who knows the problem domain to avoid/resolve details in our software: names, behavior of different components, etc.

Iterative/incremental development allows us to get the immediate feedback we need to learn. Through this, and by paying attention to details, we can discover the concepts we need to refine in our software.

Note that sometimes we will create some technical debt, just so that the software is available to the client and thus we can get immediate feedback that will allow us to recognize misconceptions. We can see the cycle here and, at the same time, its irony:

The key is finding the balance between acquiring technical debt and getting immediate feedback to recognize technical debt.

Another way to discover immature code is to look around. What does this mean? When developing a feature, let’s not just focus on our code but review the codebase related to the new feature we’re implementing:

Code review the neighbourhood. The software we develop is constantly evolving, and it’s everyone’s responsibility to ensure this evolution is consistent and of good quality. What better way to find immature code than by reading the codebase related to the new feature we’re implementing?

Our system code helps us reason about the problem domain (otherwise, we’d be writing in Assembler 😉). So, if our code is difficult to understand due to its complexity (because it doesn’t represent world reality), it’s very likely that we’re dealing with immature code and, therefore, we have accumulated technical debt.

Either we have immature code by the process of learning or deliberately, it is good to keep track of it. And this brings us to the next section.

Where did the immature code go?

We’ve already seen two tips for finding the immature code. Next, let’s keep an eye on them.

A (simple) list

A first (and easy-to-implement) way to keep track of immature code and related techical debt is to list them. The list should include all the necessary information, from the reason for the technical debt to possible solutions (if known). To have the components affected or contributing to the debt is a plus. In the Formula 1 simulator example, we might have an entry in the list describing that the name YellowFlagCar is incorrect, that its name should be SafetyCar, and that the F1Race class is based on that immature code.

This list must be up-to-date (this being its weak point) and shared with the entire team. It’s essential to review it (for example, at the start of sprint planning) so the team knows the status of them.

Comments

I’m not a fan of comments. I think code should be descriptive enough to avoid using them. For example, if we have the following code:

# if the car has a mechanical problem, which could be caused by
# an engine problem or ... or ...
# <a thousand lines of comments later>
if f1car.engine.current_top_speed < f1car.engine.top_speed || ... || ...
  # then we should request a pit stop by asking the team with
  # the lap in which we want to make the pit stop.
  f1car.team.request_pit_stop(race.current_lap + 1)
end

Note that the comments precede the code smell, showing how comments can contribute to less descriptive code (since the semantics of our code have been replaced by the comments themselves) and also lead us to bad practices (as in this case: breaking encapsulation).

But back to our present topic, in practice, comments to tag immature code can be convenient. In fact, they can be quite useful when performing a code review (which is no small feat). We can use tags like TODO, FIX, or custom tags, for example, TECH-DEBT, IMMATURE-CODE (maybe a combination 🤔 TODO:TECH-DEBT: - I never used it, but it might work).

Continuing with our example, assuming we already know the correct name SafetyCar, but want to move forward, then we can add a tag to our code:

# TODO:TECH-DEBT: Rename to SafetyCar.
class YellowFlagCar
  getter speed_limit : Speed

  def initialize(@speed_limit); end    
end

As we can see, these best practices are not mutually exclusive, and their use depends on the development team’s comfort level in applying them.

Future Work(?)

Let our imagination fly: I’d like to be able to add code annotations in the IDE (like pop-up windows) to highlight immature code (which translates to comments outside the IDE).

And, if I may ask for more, I’d like to be able to see code snippets creating a visual map of immature code. 🤩

When to improve immature code?

After what we’ve seen, this is the big question… the crux of the matter if I may.

To answer this question, we can try the following approach: Let’s start by considering the scenario of continuing to build our system based on immature code. What would happen if we continued moving forward for a considerable amount of time (e.g., three sprints)? (ie. the worst case scenario).

As we’ve already discussed, the software will become increasingly distant (sprint by sprint) from our knowledge of the problem domain (and world-reality). This, in turn, makes it difficult to continue working on it. In short, we would be in the same situation that Ward Cunningham found himself in, when, to explain/justify the code refactoring they had to implement, he coined the metaphor Technical Debt!

In our case, after three sprints, we have a lot of code to fix (again: worst case scenario).

In this situation, I can think of the following options to continue:

Note that this approach of leaving immature code to be fixed at some point in the distant future makes it difficult to know the full impact on the code base, as well as coordinating the code improvement with the project’s natural progress.

A project, an experience…

In the last project I was involved in, we took a more proactive approach by getting into the habit of improving the code base at two points in a development iteration: at the start of an issue and the week after the sprint.

Let’s take a closer look at how we did it:

The good neighbor

When starting an issue, a good practice is to read the tests and code related to the problem we’re addressing (we can also test the related features as an end user would). This helps generate a mental map of the context. In doing so, a first opportunity arises to implement small improvements to the codebase:

I call it “the good neighbor” because sometimes in standup meetings I would start by saying “Since I was in the neighborhood, I implemented the following improvement…”. In other words, since I was reading code related to what I was working on, I did my best to leave it cleaner (ie. better) than I found it.

By the way, the good neighbor approach is related to what we mentioned earlier Code review the neighbourhood. It’s important to note that these improvements shouldn’t take a long time since they are being implemented in the context of an issue.

For improvements that might take more time, there was another opportunity …

The QA week

Typically, each sprint lasted two weeks. On the last Thursday or Friday, the sprint-ending tasks were completed, and new features were deployed to the staging environment server. During the following week, while the QA team was testing the software, the development team began improving the codebase (ie. taking care of the code quality). And here comes the second opportunity for handling immature code by following the same tasks we listed in the previous good neighbor.

This approach had the great advantage of not changing the work context, as we were improving the code we had just implemented. Another advantage was greater synchronization between QA and Development teams, who worked simultaneously on software quality (the former from the end-user’s point of view and the latter from the developers’ perspective). Occasionally, the development team would fix the cause of a bug simultaneously discovered by the QA team.

Again, this isn’t a silver bullet. It’s a development cycle that emerged naturally over time.

Regardless of the development cycle we are using, the important thing to keep in mind is the following:

Code Quality: Keeping code quality should be an essential part of the development cycle (just like implementing new features).

Note: we may encounter technical debts with a significant impact on our codebase. In these cases, we may need more extensive code refactoring and a different approach. Also note that these approaches primarily focus on continuous code improvement (not only immature code).

Going back to the initial question of When to improve immature code?, in my humble opinion, based on my experience: as soon as possible!

Can we avoid immature code?

As we’ve seen, iterative/incremental software development cycles involve learning, so immature code is to be expected. Trying to avoid it would mean mastering the problem domain before implementing the solution. In other words, we would miss an essential part of the building software process: allowing the software itself to help us reason about the problem domain.

On the other hand, this would result in a long period of time before the client could see something working. Added to this, is the risk that our knowledge of the problem domain (studied before beginning to implement the system) differs from what the client understands, resulting in wasted time and lengthy meetings trying to converge ideas.

So, answering the question Can we avoid immature code?, my answer is the following question: Why?

Although it may seem ironic, we must reinforce the importance of immature code, as it remains an important first approximation to quality code and, ultimately, to quality software, which, after all, is always our goal… or should be! 🤓🎉