The Two Primary Causes of a Bad Codebase
In our years of experience working with software development teams and performing code reviews, we've observed that most problematic codebases can often be traced back to two fundamental issues:
- Lack of formal coding guidelines.
- A constant pressure to deliver features without sufficient refactoring, often also without proper automated testing.
The Problem with Missing Coding Guidelines
When a team lacks formal coding guidelines, each developer effectively becomes free to implement code however they see fit. While experienced developers often try to match their style to existing code, this approach has significant consequences:
- The inconsistency leads to ambiguity and confusion (obviously).
- It makes onboarding of new team members more difficult.
- It leads to "technical debt".
- It makes refactoring more difficult.
- It makes internal code reviews less adequate.
Ultimately, it leads to reduced productivity, longer timelines, more bugs and higher maintenance costs.
But good coding guidelines go far beyond mere style and formatting preferences. They establish an approach to development practices that align with the companies' values and objectives.
What Effective Coding Guidelines Should Contain
Effective coding guidelines should balance standardization with practicality. They should prioritize readability and maintainability while recognizing that some complexity is inevitable.
We must also accept that what we think is a good idea now may turn out to be a mistake over time. Hopefully we will learn something from this.
Beyond basic formatting rules, comprehensive guidelines should address:
- Testing strategy: how and when to test what source code. We find that without proper guidelines, mainly the east to test code gets tested. Obviously, the focus should rather be on error-prone or critical code that does get changed.
- Version control: when do we commit, how do we branch, what do we call the branches, do we use pull requests etc.
- Logging: What do we log, how do we log, when do we log and how do we decide what level to use.
- Management of environments: And not just the standard development/test/production separation, but also: how do we avoid accidentally calling external production APIs from dev/test, or sending all our customers a test email.
- UI/UX consistency: placement of interface elements (such as consistent positioning of "Cancel" buttons), modal vs inline, interaction with forms, tables, etc.
- Tone and voice: what language and tone do we use in all kinds of messages to users, such as notifications, emails and error messages.
- Design: Consistent application of colors, arrangement, typography and other design elements.
- Use of production data: If necessary, how to do this in a secure manner.
- etc
Coding guidelines should be treated as guardrails. Exceptions are possible when necessary. If there are too many or recurring exceptions then it may be time for a re-evaluation.
We view coding guidelines as a living document that evolves with the team, project and changing technology.
Are there no coding guidelines in the organization yet? Then it is better to start with a concise document with style and formatting rules than to cover everything right away. Let the documentation grow when the need arises, and when it becomes clear what is desirable or what should be avoided.
Sources for Coding Guidelines.
When developing your own coding guidelines, you don't have to reinvent the wheel. There are many excellent open-source guidelines that can serve as a basis.
Some examples:
- Airbnb JavaScript Style Guide
- Airbnb Ruby Style Guide
- Google Style Guides
- Ruby Style Guide
- Awesome Guidelines Collection
- WordPress Coding Standards
The Feature Treadmill: Delivery Without Refactoring
De tweede grote bijdrager aan slechte codebases is de onophoudelijke druk om nieuwe features "asap" te implementeren zonder dat er tijd is voor enige vorm van refactoring.
"Can you add X quickly?" or "We need a small change to Y by tomorrow" may seem harmless in themselves. However, 100's of such minor modifications piling up are disastrous and lead to "technical debt".
And technical debt works like ordinary financial debt: if you don't pay it off, it gets bigger and ends in bankruptcy.
Note: From a business perspective, it may sometimes be necessary to check off a list of features as quickly as possible, which may then cause technical debt. There is nothing wrong with deliberate, controlled technical debt. Better a profitable company with technical debt, than a bankrupt company with the perfect codebase.
How Technical Debt Builds Up
Technical debt usually grows according to a predictable pattern.
Developers implement quick fixes rather than proper solutions. They do so to meet tight deadlines, satisfy their managers, or brag about their coding skills. These fixes pile up into an unstable tangle that completely lacks any kind of uniformity or logic. The cycle continues with promises that "We'll clean it up later," but "later" never comes. The technical debt continues to grow and becomes increasingly difficult to address.
As the codebase becomes more complex, developers no longer fully understand the code. Each tweak leads to more and more exceptional bugs that are increasingly difficult to detect and simulate. Developer hell and a major risk for the project.
Why Refactoring Gets Delayed
- It does not provide directly visible functionality, it just seems to cost time and money. Non-techies often don't understand the added value of refactoring. (And sometimes there is none)
- The state of existing source code is not always clear when making a time estimate. Often one notices the need for refactoring too late, and performs the work without refactoring. Hello technical debt.
- Fear of introducing new bugs. Without solid automated testing, refactoring becomes risky. This creates a negative feedback loop.
- Sometimes the knowledge/skill is also just not there to do a refactoring, or the situation is so out of control that no one wants to do it anymore.
You also shouldn't refactor for the sake of refactoring. If it is stable code that never gets touched, refactoring has little added value.
But Technical Debt can be remediated!
-
Build refactoring into the process:
- Include 10-20% refactoring time in each estimate/sprint planning.
- Do look at the existing source code when estimating so you can plan for refactoring if necessary.
- Apply the "boy scout rule": leave source code better than you found it. Add automated tests if necessary.
-
Be clear in your communication:
- Explain technical debt to non-tech people using recognizable metaphors. My favorite: technical debt is like fixing a leak under your sink with a bucket instead of a proper repair. It works for now, but sooner or later that bucket will overflow, mold will grow in your cabinets, your downstairs neighbor will have water damage, and that mold will make you sick.
- Try to measure and visualize bad code in terms of bugs and slower delivery. Can you show how a part plagued by technical debt goes through more tester-developer cycles? Or after testing there are still more bugs found, in increasingly weird edge cases?
- Always state clearly when refactoring is needed. At the very least, stakeholders should be aware of it.
-
Take it step by step:
- Focus first on high-value, high-impact areas that are plagued by bugs or undergo frequent changes.
- If they are not already there: implement automated tests before refactoring and make sure they work with the current source code. Try to make the tests complete: test not only the "happy path" but also historical exceptions and bugs.
-
Integrate it into the culture:
- Reward quality over sheer speed. The developer who consistently produces virtually bug-free code should be the role model for the team. However, they often stand out less than the cowboy/cowgirl who implements new features who turned out to be plagued with bugs later on.
- The mindset "it shouldn't be bug free right away, the tester should also have a job" must be strictly countered. There are teams that work without manual testers and still manage to produce solid code. The safety net of a tester too often creates a lax attitude.
- Are automated testing practices not yet established? Then start there. If necessary, engage a consultant to work with the team to take the first steps.
- Successful refactoring efforts may also simply be named, recorded and celebrated. Just like development that management or customers do see.
-
Make Technical Debt Visible:
- Provide code with comments/tags such as NEEDSREFACTORING or NEEDSTESTING so people can take them into account when estimating and modifying. It also helps when someone has some extra time (does this ever happen in real life?) to quickly locate the code that could benefit from refactoring or testing.
- Create a "Technical Debt Register" to record problematic code and its impact. By documenting changes, bugs and failed tests in the process, more informed decisions can be made on how to address such problem areas.
Conclusion
A healthy codebase always benefits from clear coding guidelines and deliberate refactoring practices.
De voordelen op lange termijn zijn aanzienlijk:
- Faster onboarding of new team members.
- More predictable estimates and development times.
- Fewer problems during testing, faster flow to production.
- Fewer bugs and production incidents.
- Better and faster response to changing requirements.
- Improved developer satisfaction and retention.
The quality of your codebase is a direct reflection of your development process.
Improve the process, and the code will follow.