All too often we dive into a codebase to find a mess of spaghetti code, unused features, or features that don’t fit with the rest of the product. It’s easy to look back on a project and see where we spent too much time perfecting or where we built something we didn’t know we needed.
We can mitigate these risks by following the steps Kent Beck proposed for test driven development:
- Make it work - focus on the most basic behavior that works
- Make it right - focus on making it right - refactoring, removing duplication, architectural/structural, aesthetic, consistency, patterns, “clean code"
- Make it fast - focus on performance where needed
Make it work
We focus on making it work before focusing on making it right and fast because if it doesn’t work or feasibly won’t ever work, then making it right and making it fast are irrelevant. Why bother spending the time and money on making it right and fast in that case? Making it work can be challenging because first we have to define what working behavior we actually want. Sometimes after making it work, we test it with users and determine that it’s not actually what we want or need, so progressing to stages #2 and #3 are also unnecessary.
At this stage, there are many unknowns in the tech stack, in the product and the value it will offer, and occasionally in the team composition. We don’t know enough to make all the perfect choices for each feature we are building (let's face it, it'll never be perfect). Instead of perfection, we simply make it work according to the behavior we defined. Patterns will emerge from this. We might experiment with different approaches to the same problem in different places in our codebase and find out that we prefer the second approach, however we won’t go back and fix something that is currently working until we get to the make it right stage. That’s when we know just enough to be able to make that decision.
Consider the image of the mailboxes above. We know that the mail carrier needs to know the number of the box and that the box has to have a door. Those are the minimal requirements to make it work. These mailboxes are also relatively consistent as well. They all have readable numbers, they have doors, they're all about the same size, they're all at the same height. The mail carrier knows how to use them and they are functional. They are also not boring at all!
Make it right
Once it works, it’s important to circle back and make it right. If you don’t you’ll pay for it in the long run. Making it right helps to ensure the longevity of your technology by making it easy to understand, extend, scale, and maintain.
Make it fast
Now that you have reduced the unknowns both in your tech and in your business, you know where you’ll benefit from performance improvements and where you won’t. It’s time to make it fast and focus on the areas where you’ll benefit. Maybe it's more important to make a process for your customer's faster, but ok to have a slower background process that no one sees.
Don’t make a complete and total mess
It’s important to understand that “make it work” doesn’t mean “make a complete and total mess”. Making a mess makes making it right and making it fast much more expensive. This can happen for a number of reasons, such as a team not working as a team, not having a basic overall strategy for going about the problem, having too many inexperienced team members and not enough experienced team members, or having a person on the team who is just plain careless.
Careless team members don't care enough to not create a mess. When members of the team are all on a different page in terms of how to solve a problem or similar problems, or they aren't communicating with each other, you can end up with [insert the size of your team here] ways of solving the same problem in your codebase.
Experienced team members know where to make trade offs in design, architecture, and implementation. They know how to make those decisions and are intentional about doing so. They even have the basis for a plan in the back of their mind for how to tackle making it right and making it fast. They have spent enough time on making it work, making it right, and making it faster that they have the experience to know what is important during each stage.
Less experienced team members haven't necessarily solved these problems for a production environment, they may not be familiar with design patterns, or libraries that just do it for you. They might not understand the tradeoffs that come with deploying a mess of code to production because they've not worked with real users and had to fix bugs at 3am. They might not understand how much a production bug costs versus a development bug.
Knowing where and how to make these trade offs is key.
So how do you make it work so that you can eventually make it right and fast? Well, it depends! Here are some examples:
- Communicate. Make sure your team is communicating with each other. The team does not have to be made up of all experienced developers. A team diverse in skills, experience, and background will create a well-rounded product. However, the team needs to communicate. Likely, there needs to be some form of mentoring or pair programming so that less experienced team members gain more experience and more experienced team members get that second set of eyes on a problem they are over-engineering.
- Make sure the tools and frameworks you choose allow your developers to move fast when making it work. If they are hindered from day one because they are not set up with standard best practices for writing, testing, and deploying code, they will move slower. This is where using a framework or tech stack where this is already defined for you comes in handy.
- Be consistent in solving problems. Don’t pull in three different libraries that accomplish the same thing in three different ways just because you can or because there is some new shiny library you want to try. Consistency in how you solve similar problems is important when you have to pull in another developer or when one leaves. Or when you go to make it right and now have to decipher why you solved X four different ways. It’s much easier to comprehend a consistent code base that works but has no documentation versus an inconsistent codebase that only sometimes works and has incorrect documentation. Consistency is also important when you move on to stage #2 and #3.
- When you do want to introduce multiple tools, frameworks, or approaches to accomplish the same thing in different ways, that’s cool. Just be intentional about it with the expectation that you’ll circle back to make it right. Do it in a way where you can evaluate both and then choose the best one for the product in the make it right stage.
- Work on vertical slices during the make it work step. Think of “tracer bullets”. Make sure that the problem you are solving with the tools you are solving it with is actual feasible. You don’t want to get to the very end and find that the library you were going to use doesn’t work for the last chunk of your work. Figure that out sooner rather than later.
- Seriously evaluate whether your "careless" team members are worth keeping around. Are they adding value to your product AND your team?
This is only scratching the surface and everyone's experience, team, product, business, and tech are different. Focusing on the make it work stage to start with is important, but still take care so that making it right and making it fast are actually possible.