16 Comments
User's avatar
Fabien Ninoles's avatar

One thing that has helped some of my team was to divide the bug tracking process in three:

- initial bug investigation, to know what's going on

- initial fix, to fix the problem

- final fix, which includes refactoring so the problem is less likely to happen in the future

The trick here is to not consider the big fix until all three steps are done. That's also required to consider bugs as top priorities over features.

Similarly, I consider individual refactoring tasks as ill-conceived. Refactoring should always be part of the delivery of a feature. Putting them aside make them optional, and so easier to skip. One should even consider doing them before working on the feature itself.

In the above, it's also important that the team is able to deliver in very small chunks. Each delivery should be taking less than a few days, ideally no more than a day of work. It takes times for team to be able to achieve that, but such a constraint is probably the strongest incentive to create a maintainable architecture.

Expand full comment
Valentina Jemuović's avatar

"Similarly, I consider individual refactoring tasks as ill-conceived. Refactoring should always be part of the delivery of a feature. Putting them aside make them optional, and so easier to skip. One should even consider doing them before working on the feature itself."

--> Spot on! Exactly, we should not be doing refactoring for the sake of refactoring, but rather to make the next change easier. So that means, if it's too cumbersome to implement a feature, because our code is not well designed, then as part of that feature delivery, we firstly refactor and then implement the actual feature. As Kent Beck said "for each desired change, make the change easy... then make the easy change" (https://x.com/KentBeck/status/250733358307500032?lang=en).

"In the above, it's also important that the team is able to deliver in very small chunks. Each delivery should be taking less than a few days, ideally no more than a day of work."

--> Yes, working incrementally is crucial. We deliver value earlier, and we get feedback earlier.

"final fix, which includes refactoring so the problem is less likely to happen in the future"

--> Could you clarify, provide an example...

Expand full comment
Fabien Ninoles's avatar

A small note: I found most team understand iterations. What they miss is that the iteration can be much more smaller than they think. The reference text for this is for sure the série about MMMSS from GeePaw Hill (https://geepawhill.org/series/many-more-much-smaller-steps/).

Expand full comment
Valentina Jemuović's avatar

Yes, teams need to firstly understand how to think iteratively at a functional level (User Stories should be as small as possible yet valuable), and iteratively at a technical level (commits should be as small as possible).

Expand full comment
Fabien Ninoles's avatar

For the fix in multiple parts, it's hard to give a good example, but here a naive one:

You get a crash, upon investigation, you find out is a null referencing pointer. Quick fix is to add a guard (if pointer not null) before dereferencing. You do it now, so the code no longer crash.

The code don't crash but the design isn't fix yet. So, you work on the good fix, to avoid a null pointer, maybe by replacing it with a null object pattern or always having a good default at construction.

Expand full comment
Valentina Jemuović's avatar

Good example. It reminds me of the “Bug Free by Design“ concept.

Expand full comment
Jelena Cupac's avatar

Quick fixes are like duct tape - great for the day, terrible for the year. But how should you refactor a big legacy codebase without breaking production?

Expand full comment
Valentina Jemuović's avatar

To ensure that we don’t break production, we should ensure that we firstly have a Pipeline and System Level Tests (Smoke Tests, Acceptance Tests, External System Contract Tests, E2E Tests). That’s what gives us safety in protecting us from regression bugs.

Then with that safety net, we have the freedom of redesigning our architecture. Then, inside a component, we would introduce Component Tests & Contract Tests, which gives us resaonbly feedback when refactoring. However, if we want even faster feedback, we would redesign the Component to be unit testable (e.g using Hexagonal Architecture) and we’d have the protection from Component Tests that we didn’t break anything… then we’d be able to introduce Unit Tests & Narrow Integration Tests. At this point, whenever we refactor, we’d get the fastest possible feedback.

Expand full comment
Per Lundholm's avatar

Reuse is also coupling. A React component was once used in two places. Changed it for reasons in one and broke the other.

Expand full comment
Fabien Ninoles's avatar

That's often pointed out to a confusion between form and semantic: the code can look the same, even be the same, but the usage is semantically different. It can be hard however to see the difference, and it's why SOLID has the Open/Close principle: open to extension but close to change. If you made your change as an extension above, it wouldn't have been breaking the other users of the code.

Expand full comment
Per Lundholm's avatar

Definitely. Not that many understands this or a aware about SOLID.

Expand full comment
Fabien Ninoles's avatar

I would say that, for a lot people, design in all forms (tech, UX, UI, etc.) is just a bunch of recipes from which to choose based on what you fill rights, much like a restaurant à la carte menu: you don't care about how the meal is prepared, how it contributes to your experience, and even less if it's nutritious and balanced. You just choose based on your past experiences and personal taste rather than on what would make the most sense for the current meal.

Understanding why something works or not, what forces and constrains applied to it, and it enables or disallow is very important to good design. Unfortunately, most of the time, people are just unable to answer not only what a specific design provide, but also what their application actually needs.

Expand full comment
Valentina Jemuović's avatar

Insightful discussion. Yes, we should try to understand what’s the problem we’re trying to solve, what goals we have, what constraints we have, and then choose the optimal design… rather than just sticking any design.

Expand full comment
Per Lundholm's avatar

Software is so valuable that it is possible to operate at that level of ignorance and still be acceptable

Expand full comment
Valentina Jemuović's avatar

It’s a shame that software engineering isn’t treated like other forms of engineering.

Expand full comment
Valentina Jemuović's avatar

Good point, regarding limitations of reuse, that if a reusable component needs to be changed due to one client, it could then be incompatible with other clients.

Expand full comment