Many modern software teams do not give enough attention to the quality of their work. We spend a lot of time improving our processes of how we write and deploy code and delivering great products that our customers love, but we often overlook the quality of our code.
What we overlook is not necessarily the language or technologies that were leveraged or even the architecture that was used, but specifically HOW the code was written. Our code is riddled with “old” patterns, undocumented methods and properties, “hacky solutions,” hidden dependent relationships, etc. When our apps and teams are small, this is a minor inconvenience, but as we scale, this quickly becomes a major problem. This can lead to features taking much longer to build, engineers who are anxious to make changes in certain areas of code, high-risk refactors that can take weeks or even months to complete, tech leads leaving to work at startups with less technical debt, and many other issues.
At Kino, we want to be happy with the code that we’ve written for years to come. This does not mean that our patterns and technologies will not change, but we proactively work to build quality-in. This, in turn, will reduce the usual discomfort felt when working in “old” code.
We do this by following four main principles.
Four Principles of Quality-in Code
Principle 1: Follow good patterns and principles
What are good patterns? The term “good” could have many meanings: it could be as granular as how you handle null-checking, to as broad as the structural design pattern that your platform should be following.
How do you decide what patterns your team should follow? This should not be done by a single person. However, it should be facilitated by a strong technical leader – the technical leader should help guide the conversation to keep it relevant and keep the team from choosing to adopt bad patterns. Oftentimes, two patterns differ only by preference. In these cases, the team should decide the preferred method.
Don’t expect to have all of the patterns documented in a single meeting. This document should be a living document. As a team member identifies divergent patterns across squads/areas of code, they should raise their hand and request that the team gets together to decide on the best pattern to follow moving forward.
With technology teams building complex platforms, each area of the code must be architected and organized the same way, e.g., using the same authentication strategies for securing routes. If the same patterns are not followed between the different areas, engineers lose time as they research the new strategy and switch contexts between those areas. Furthermore, all of the different strategies take up a lot of brain space.
Team principles are similar to patterns but are more implicit. They refer to guidelines on how we should code. Here are a few examples of some of the team principles we follow at Kino:
1) Leave it better than you found it: Oftentimes, we come across code that doesn’t align with our patterns. This does not mean that we should always stop what we’re doing to refactor. However, there might be some comments that can be improved, or some TODOs that could be added, or future technical debt work that could be lined up.
2) Write testable code: This doesn’t mean that we’re going for 100% code coverage, but everything should be written in a way that it could be tested.
3) Spend the appropriate time designing upfront: Having a good understanding of what you are building and how you are building it will always save you time.
4) Get feedback as early as possible (both product and technical feedback): If you wait until you submit a merge request to get feedback on your technical decisions, you may end up doubling the amount of time spent in order to refactor the work that you had already completed. A simple conversation earlier could save your hours or even days of work.
5) No “hacks” to get the job done quickly: The caveat to this, is when there is a critical production bug, and you can throw in a quick fix to solve the problem but this should always be partnered with technical debt work in the backlog to fix the “hack.”
6) Don’t overcomplicate simple solutions – use the right tool for the job: We have all been guilty over-engineering a solution. One example of this is creating an asynchronous method when there is no need for it. Sometimes abstract classes unnecessarily complicate how we reason about code.
7) Backend APIs should not be customized for a frontend experience: Our backend APIs should be generic and reusable. If we want to convert an enumeration to a human-readable string, that conversion should be handled by our frontend, not our backend.
8) If you don’t know what it does… don’t copy it: We should fully understand what each line of code we write does. It’s ok to follow a pattern from code someone else has written, but don’t copy anything that you don’t understand.
Having agreed upon team patterns and principles are key to having a successful team. Without them, there will be no guidance on how or why things should be built a certain way. Each developer will build things according to their preference instead of what had been decided on by the team.
Principle 2: Create knowledge
“If it was hard to write, it should be hard to read” – A developer you probably know
The mindset above is not uncommon in our industry. We think that our secret knowledge gives us “job security” or a certain amount of power and seniority. It does cause others to come to us for our specific domain knowledge, but mostly it shows how junior and immature we are. At Kino, this behavior is not tolerated. Instead, we focus on creating knowledge. We do this through show ‘n tells, peer reviews, shared documentation, and comments. We empower teams and individuals to make good choices by giving them access to the information. We do not believe in single points of failure.
Comment! Comment! Comment! and then Comment some more! When I was still in school, my professors graded us not only on whether our code worked as expected but also on the comments we added to our code. It trained me to comment extensively and – more often than not – I had more lines of comments than I did code. However, as soon as I started my first job that all went out the window. The apps at my new company barely had any comments in them, so I quickly picked up this behavior. After only a short time, I adopted the pattern of minimal comments. This is why the team needs to adopt the pattern of good comments. Just because we have a Sr Engineer, we can’t expect them to have good commenting disciplines.
What is a good comment? A good comment explains not just what we are doing, but WHY we are doing it. A comment that simply states that we’re short-circuiting our function if a value is null is not nearly enough. It gives no intent behind the work, simply states what we are doing.
Why are we short-circuiting the method? Why aren’t we throwing an exception? What if I want to come in and change the logic for this null case, do I introduce some hidden risk by changing this code?
Comments should not just be in our code, but they should exist in our tests, our styles, our CI/CD build scripts, and our templates. It’s important to know why certain styles are applied or why we have some condition in our template. Don’t be shy about adding comments everywhere.
Sometimes we must divert from our team patterns because our patterns can’t cover every single scenario. When this happens, we should always add a comment explaining that it is an anti-pattern and why we had to diverge from the team patterns. This is important for two reasons: 1) a developer may look at this code in the future and attempt to refactor it, not knowing it will not work and 2) a new engineer who does not realize it is an anti-pattern may copy this pattern in other places.
When we come across undocumented things, it increases our cycle time because we must read the code to figure out what it is doing. Sometimes, we must find the places where it is being used, and sometimes, it still just doesn’t make sense, and we have to reach out to the author. We should always add what we’ve learned in our comments for the next developer. Otherwise, we can’t capitalize on the time we’ve spent doing the research.
Principle 3: Serve each other with our time
Even if we adopt the most bleeding-edge processes and read all of the books by all of the experts, we still tend to be quite selfish developers. This is not usually intentional. We have every intention of being a good teammate, but we often miss the subtle yet crucial part of lean principles. For example, cycle time is extremely important. We want to get feedback from our customers faster so that we can make wise decisions for our product. So as an engineer, if I focus on my cycle time, I might skip unit tests, skimp on comments, or take some other shortcuts. This doesn’t decrease our cycle time. It just defers that cycle time, often making the team’s cycle time as a whole much worse.
At Kino, we talk about our lean principles every day, but that doesn’t count for much if the team doesn’t fully understand what those principles mean. Here are some ways that we try to serve each other with our time:
1) Identify risks and additional testing needs during development, and document them in our tickets. If I make a change in a file that is shared by another service the tester has no way of knowing that there should be additional regression testing unless we tell them.
2) Test edge cases before submitting a ticket for feedback. It takes less time for you to verify them upfront than for issues to get discovered during the feedback phase and get passed back to you.
3) You should be sure that your work is perfect before moving your ticket down the line.
**This does not mean you should not get feedback on your work during development.
4) Get feedback on your work as early as possible. If you are going to build out a new feature, get input before you start writing code. If you spend two days writing code and find out during the peer review that you have to refactor it, then those two days are wasted. If a certain UI/UX will not work how it is designed, don’t “solve” the problem by yourself and then ask the PM if it’s ok when you pass it to feedback.A 10-minute phone call could save you 15% or more on… wait.. I mean a 10-minute phone call could save you HOURS or even DAYS of development time.
5) Don’t “skim” on peer reviews. Peer Reviews are about more than making sure the file is indented correctly and that all syntax standards are adhered to.Make sure you understand what is being accomplished and how those changes achieve the result.
Read the documented work item/story. Do you know what the change is before looking at the code? Did they do it the way you would have? If not, have that discussion. Everyone will learn something. Large PRs are overwhelming when comparing differences. Instead, hop on a call and have the developer walk you through the code and discuss the changes.
6) Help Test. If you see something in a PR that you think might be an issue, go test it yourself to see. Help Identify risks and make sure they are documented in the ticket to be tested.
If there are changes that need to be made that will add risk to the work, the ticket should go back through the feedback and testing phases. Write unit tests to cover the scenarios for all bugs so that they don’t happen again.If a test is not possible, that demonstrates the code needs to evolve to keep up with the complexity of added features. Make sure your code is covered reasonably well with tests.
At Kino, we don’t strive for 100% code coverage since that usually means the tests being written are not valuable.
- We write tests on code when bugs are discovered.
- We write tests for any high-risk code.
- We write tests for any shared code.
Principle 4: Spend time as the customer
As developers, we often use our apps as… developers. We test the feature we’re building, but not the application as a whole. We should spend time using our product as our users do. We should understand the various personas that use the system, and use the product while in the mindset of that persona.
We should try breaking the product. As developers, we tend to know where our most “brittle” areas are, and can probably come up with ways to break our product. It becomes a game. If you can break it, that’s a good thing. You have identified an area that needs some TLC. If you understand the real-world state, then you have some intelligence on how to break the product. Test the limits and boundaries of the product because your users definitely will.
The Kino Results
At Kino, we believe that if we all practice these principles, we will have a codebase that we can all enjoy working in, and we can continue to be able to iterate and move quickly, even at scale. New engineers will come on board with a plethora of information on how to follow these principles and be successful. This will lead to less thrashing, shorter cycle times, and more shared knowledge, which could be the determining factor for a company’s success.