Two Major Causes of a Poor Codebase

We notice that the cause of the most problematic codebases can often be traced back to 2 fundamental problems:

  1. Lack of formal coding guidelines.
  2. Constant pressure to deliver features without sufficient refactoring, often also without proper automated tests.

The Problem of Missing Coding Guidelines

When a team has no formal coding guidelines, every developer gets free rein to implement source code however they wish. Wild growth, in other words. Experienced developers might try to adapt to the style of existing code. But often there simply won't be any consistent style to discover in the existing code. Concretely, this has the following consequences:

  • The inconsistency leads to confusion and bewilderment (obviously).
  • It makes onboarding new team members more difficult.
  • It leads to 'technical debt'.
  • It makes refactoring more difficult.
  • It makes internal code reviews practically impossible.

Ultimately, it leads to reduced productivity, longer timelines, more bugs, and higher maintenance costs. Nothing good comes from it anyway.

Good coding guidelines, however, go beyond just style and formatting. They establish a standard for development practices that align with the values, objectives, and size of the team and organization.

What Effective Coding Guidelines Should Contain

Effective coding guidelines must balance uniformity with practical applicability. They should prioritize readability and maintainability while recognizing that some complexity is inevitable.

We must also accept that we don't have a monopoly on wisdom. What we now think is a good idea may prove to be a mistake over time. Hopefully, we'll have at least learned something then.

Besides style and formatting rules, comprehensive guidelines could address the following:

  • Testing strategy: How and when to test which source code. We notice that without guidelines, mainly the easily testable code gets tested. Obviously, the focus should rather be on error-prone or critical code that gets modified from time to time.
  • Version control: When do we commit, how do we branch, how do we name branches, do we use pull requests, etc.
  • Logging: What do we log how and when and at what level.
  • Environment management: And not just the standard development/test/production separation, but also: how do we avoid accidentally calling external production APIs from dev/test, or having all our customers receive a test email.
  • UI/UX consistency: Placement of interface elements (like consistently positioning "Cancel" buttons), modal vs inline, form interaction, tables, etc.
  • Communication tone: what language and tone do we use in various messages to users, such as notifications, emails, and error messages.
  • Design: Consistent application of colors, layout, typography, and other design elements.
  • Use of production data: If necessary, how to do this safely.
  • etc

Coding guidelines should be treated as guardrails. Exceptions are possible when necessary. If there are too many or recurring exceptions, it might 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's better to start with a concise document with style and formatting rules than to try to cover everything right away. Let the documentation grow when the need arises, and when it becomes clear what is desired or rather undesirable.

Sources for Coding Guidelines

When developing your own coding guidelines, you don't need to reinvent the wheel. There are many excellent open-source guidelines that can serve as a foundation.

Some examples:

The Feature Factory, where new features have absolute priority

The second major contributor to poor codebases is the relentless pressure to implement new features "asap" without time for any form of refactoring.

"Can you quickly add X?" or "We need a small change to Y by tomorrow" might seem innocent by itself. However, hundreds of such small adjustments piling up are detrimental and lead to 'technical debt'.

And technical debt works like regular financial debt: if you don't pay it off, it keeps growing.

Note: from a business perspective, it may sometimes be necessary to be able to check off a list of features as quickly as possible, which may then cause technical debt. There's nothing wrong with technical debt that's caused in a controlled and conscious way. Better a profitable company with technical debt than a bankrupt company with the perfect codebase.

How Technical Debt Accumulates

Technical debt usually grows according to a predictable pattern.

Developers implement quick fixes instead of full solutions. They do this to meet tight deadlines, please their managers, or show off their coding skills. These fixes pile up into an unstable tangle where any form of uniformity or logic is completely absent. The cycle continues with promises that "We'll clean it up later," but "later" never comes. The technical debt continues to pile up and becomes increasingly difficult to address.

As the codebase becomes more complex, developers no longer fully understand the code either. Every change leads to increasingly exceptional bugs that are increasingly difficult to trace. This really doesn't make you happy as a developer.

Why Refactoring Gets Postponed

  • It doesn't deliver directly visible functionality, it only seems to cost time and money. Non-technical people often don't understand the added value of refactoring. (sometimes there is no added value either)
  • The state of existing source code isn't always clear when making a time estimate. Often the need for refactoring is noticed too late, and the work is performed without refactoring. Hello technical debt.
  • Fear of introducing new bugs. Without proper automated tests, refactoring becomes risky. This creates a negative feedback loop.
  • Sometimes the knowledge/skill is simply not present to perform a refactoring, or the situation has gotten so out of hand that nobody wants to start on it anymore.

You also shouldn't refactor just to refactor. If it's stable code that never changes, a refactoring has little added value.

But Technical Debt can be remediated!

  1. Build refactoring into the process:
    • Include 10-20% refactoring time in every estimate/sprint planning.
    • Also properly review existing source code when making an estimate so you can plan refactoring if necessary.
    • Apply the "scout rule": leave source code better than you found it. Add automated tests if necessary.
  2. Be clear in your communication:
    • Explain technical debt to non-technical people using recognizable metaphors. My favorite: technical debt is like fixing a leak under your kitchen 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, more bugs are still found, in increasingly weird edge cases?
    • Also clearly indicate when refactoring is necessary. Stakeholders should at least be aware of it.
  3. Tackle it step by step:
    • Focus first on high-value, high-impact areas that are plagued by bugs or that undergo regular changes.
    • If they don't exist yet: definitely implement automated tests before refactoring and make sure they work with the current source code. Try to make the tests complete: don't just test the 'happy path' but also as many historical exceptions/bugs as possible.
  4. Integrate it into the culture:
    • Reward quality over pure speed. The developer who consistently produces virtually bug-free code should be the example for the team. Often this one is less noticeable than the cowboy/cowgirl who implements new features left and right that are plagued with bugs.
    • The mindset 'it doesn't have to be right immediately, the tester needs a job too' 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 leads to a lax attitude.
    • Is providing automatic tests not yet established in the team? Then start there. Possibly engage a consultant to take the first steps together with the team if necessary.
    • Successful refactoring efforts may also just be named, recorded, and celebrated, just like developments that management or customers do see.
  5. Make Technical Debt visible:
    • Provide code with comments/tags like NEEDSREFACTORING or NEEDSTESTING so that they can be taken into account in estimates and modifications. It also helps when someone has some extra time (does this ever happen in real life?) to work on refactoring or automatic tests.
    • Create a "Technical Debt Register" to record problematic code and its impact. By documenting changes, bugs, and failed tests, more informed decisions can be made about addressing such problem areas.

Conclusion

Coding guidelines and deliberate, timely refactoring always benefit a codebase.

The long-term benefits are significant:

  • 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 satisfaction and retention of developers.

The quality of your codebase is a direct reflection of your development process.
Improve the process, and the code will follow.

Doubts about your source code?
We bring clarity.

Non-committal conversation?