How I think about Code Management
You might have the worst codebase in the world.
It was written by people who had no standards or didn’t care about code quality.
Here my POV:
I bet you don’t have a problem with code quality. Not even with technical debt.
Your problem is how you manage your code.
I bet this sounds obvious, but you couldn’t guess how often i talk to teams who don’t think this way.
Code Management 101
As an engineering team, your main job is to make sure you provide your customer value and help the company make money.
Your second job is good code management to be able to do the things above.
You can split code management into two tasks:
- Reducing complexity
- Increasing confidence
And you do both over a continuous process.
Every advice you will ever read about improving codebases falls into either of those two categories.
Let’s jump into this.
Reduce complexity
If you are unsure what the difference between complicated and complex:
- Complex means many interconnected parts
- It’s the opposite of simple
- Complicated means high threshold to solve
- It’s the opposite of easy
You solve complex problems by breaking them apart into smaller easier problems.
You solve complicated problems through eureka moments.
Almost everything you encounter in software engineering is complex problems. Let’s be honest: The truly complicated problems you usually outsource to specialized libraries.
I highly recommend this crashcourse on complexity theory by Scott E. Page
How do you reduce complexity?
You want…
- Fewer parts
- Fewer interconnections between them
You can do this in two ways.
Concepts with better boundaries
You can create fewer parts and fewer interactions by combining concepts into fewer abstractions with clear boundaries.
You know this.
Almost all abstraction concepts in any programming language are meant for this: namespaces, modules, inheritance, etc etc.
You create a better abstraction encapsulating a concept with a clear interface for it.
You switch this…
to this…
Example: The classic rails approach to this is to create namespaces that handle concepts and have a namespace level API - eg Notifications.send(for: …)
A general rule of thumb is that you should be able to write down the concepts in your app in simple English sentences. And you should be able to point at the specific representations/abstractions for all nouns and verbs.
Remove code
The other way to have fewer parts and hence less complex code is by removing code 🤷
Additionally to simply deleting:
Functionality that is provided by industry-standard libraries instead of own code does not (knock wood) need to be managed by you.
A quick note on extractions
You want to manage primarily code relevant for your specific domain logic.
This is why we all like to extract generic auxiliary functionality into our own standalone libraries with their own test coverage or into microservices.
Why are those things sometimes great and sometimes horrible?
Viewed via complexity theory:
Microsystems that are tightly coupled (means many interconnections) create hard to manage complexity.
Let’s get to part two…
Increase confidence
Confidence does not mean “ego”. It does not mean “being fearless”.
It means when tasked with a request you know how to solve it. It means you can make estimates without running into “unexpected problems” and you know how to approach problems and solutions.
How to increase confidence?
- Streamline
- Minimize the ways things can be done
- Make those ways obvious
- Make sure those ways work
- Automate anything you can
- Introduce explicit processes for anything you can’t automate
- Leave notes for things where you can do neither
- Ensure the new code you add is better than the old one
How to ensure new code is better?
You ensure new code is better through the same principles.
- You streamline
- You automate
- and you introduce process
The magic trick:
And you quantify your code quality expectations and enforce them
Autoformatting and Code Complexity Analysis is your super-power here.
Have your linter enforce below certain complexity code metrics. The classic one is cyclometic complexity scoring but there is multiple others.
This tooling helps your team to get automatic feedback and increases confidence.
But there is more to this…
Single Player Mode
A person working alone at a random hour should be able to know how to decide, where to find what they need and how to implement a solution – on their own.
Your end goal here is that engineers on their own can work productively. A concept that our folks at Product Hunt call Optimizing for Single Player Mode. I explain this in detail in this video.
A continuous process
“We really need to refactor this when we got time for it.”
~ Person who will never have time for it
You need to do two things:
- Understand which technical migrations you have
- Create time
Technical migrations
Ideally you have one way to do one thing, and your code represents the clearly the concepts of your domain logic.
But usually stuff is not ideal.
You have old code, old systems, bad implementations, previous pviots, old libraries or frameworks.
Once you introduced a new way how you do something you started a migration of the whole codebase to this.
Consider this before introducing new ways to do something. Sometimes global coherence is better than local perfection.
Taking inventory
Have a list of every migration in your codebase.
- What is the original state
- What is the ideal end state
- Have a decision/plan for each migration
Eg, migrating from framework A to framework B. Which features and parts of the codebase does this involve.
I wrote a longer post on refactoring codebases
But the gist is around:
- Make it explicit (ideally in code)
- Eg by leaving notes in the code or isolating legacy code by Prefixing it (e.g.
LegacifyNotifications
)
Create time
We all want to add chore tasks to JIRA standup discussions. But let’s be honest. If your team needs to move fast, this is rarely possible.
You might be able to justify the value of a large refactoring project if it’s impossible to get anything done in that area otherwise. But how can you justify removing the last parts of your (e.g.) redux usage just to get rid of a library dependency so that your webpack setup is more straightforward?
Refactoring projects in my experience sometimes work for the first 80% of a migration. Usually the parts that were attached to a critical feature. But rarely for the last 20%.
Before you know it, you end up with dozens of migrations that are at their last 20%, and stuff gets in your way all the time when you need to do horizontal (system-wide) changes.
Some of them are even half-done migration that happened after other half-done migrations about the same problem. E.g. multiple javascript frameworks/approaches/libraries in the same codebase.
You got into this by death through thousand cuts - you won’t get out of this by one large effort.
My recommendation: Introduce Fix-It Friday
Engineers know the migrations, know what’s important to your company right now.
Let them work on what they think is useful. Recent or old features, bad or good code. Their call.
You should see improvements within weeks, if not months.
Having it on one fixed day creates peer pressure within the engineering team and authority towards other teams and deadlines.
TL;DR
You have two jobs:
- Reducing complexity
- Increasing confidence
And you do both over a continuous process.
hope this helps