Code to live by — technical development philosophies of a successful studio
Every development studio revolves around a set of core philosophies and values that the team share in order to successfully build and deploy applications. In this article, we share some of the philosophies we follow at Magnopus UK in order to build robust cutting-edge applications that widely vary in scale and complexity.
As individuals, we all have values, principles we live our lives by, whether conscious or subconscious. These values also scale with groups of individuals; whereby values that are shared by many tend to become engendered in all, whilst those that are rarely held tend to fizzle out and die.
This is directly applicable to organisations, and the teams that they are composed of. Often these values are tribal: not written anywhere, but still upheld by the team, and at Magnopus UK we try to codify the values of the engineering team to make it explicit to all what we consider essential principles as part of our work.
By explicitly codifying our values, we as a team can agree on what we consider important, and can then share with each other, new starters, and indeed you, dear reader, what we consider important.
Always Buddy Check
The use of code reviews as a preventative measure against bugs is relatively standardised in many areas of software engineering these days. Unfortunately in our real-time, game-engine-based, niche of the industry, it is still surprisingly rare to find this practice and it is one that we value tremendously. Code reviews can take many forms, and at Magnopus UK they take the form of ‘buddy checks’.
A buddy check involves requesting any other member of the team to participate in a synchronous review of a pending changelist at the reviewee’s desk, where they will go through the change being committed line-by-line in a diff tool, review the functionality being committed in a local build and provide feedback.
Since we follow an everyone-commits-to-mainline model at Magnopus UK, it is obvious that buddy checks form our first line of defence against bugs making their way into the codebase. However, there are other benefits that we consider equally valuable:
Opportunity for learning: a buddy check can be a great tool for either party to teach the other person a new technique, improve their understanding of a system, or even to teach a new concept.
Knowledge dissemination: although documentation is our preferred method of communicating a project’s architecture, technical documentation will never cover everything exhaustively and often may become out-of-date. Buddy checks complement documentation by facilitating sharing of up-to-date knowledge of how a particular corner of the codebase works and why it works.
No Egoes
We recognise that as individuals, at all levels of seniority, we naturally have gaps in our knowledge-base, even if in other areas we may consider ourselves more of an expert. Everyone on the team needs help sometimes understanding an unfamiliar concept, but we are all smart people and capable of understanding something, if only someone would take the time to explain.
By accepting this, we promote a culture of empathic tuition. When it becomes clear a member of the team doesn’t understand a concept or is struggling with a task, it’s an opportunity to teach. There is no room for egoes or lone-wolf coding ninjas. We are fundamentally a team, and succeed or fail as a team.
Always Leave Something Better Than You Found It
In life, we generally don’t get as many chances to explicitly address technical debt as often as we would like, but it still piles up all the same.
Without any means of keeping on top of that debt, you can be sure that sometime in the future, you are going to be working on integrating a new feature and be continually blocked at every turn by brittle/inflexible technical decisions made in the past that now would require a Herculean effort to fix before even getting to your feature.
The thing is, we have daily opportunities to chip away at technical debt. Whatever feature you are currently working on, it is likely that as you work you may notice some pre-existing code that could do with a minor improvement. Whether it is simplifying a local function definition, fixing an edge case, or simply fixing grammar, these minor incremental changes add up to huge improvements to the codebase over time. Therefore, this philosophy becomes our first line of defence against technical debt.
Always Fix The Root Cause Of A Bug, Never The Symptoms
When engineers are tasked with fixing a bug, they often face a dilemma once they reach the point in their investigation where they have identified how the bug manifests. Do they fix the manifestation, or do they keep digging to understand how the bug occurred in the first place? It may seem obvious that you should try to always do the latter, however, you’d be surprised how often people elect for the former.
This is likely related to the fact that it requires more effort to keep digging and find the root cause, and so at some level of consciousness, an engineer elects to take the path of least resistance. But the thing is, a bug can never really be fixed unless you first understand the bug. Why does it happen? What path does the software take to get there? Once you can answer these questions, the fix often becomes obvious.
Build System Should Be The Only Source Of Truth
Builds generated by CI are the team’s guiding light throughout the development of a project. When working on a feature for a project, we cannot say that it is complete until it has been buddy checked, committed, had a build generated by our CI and the feature has been reviewed in the build. In this way we eliminate the philosophy of ‘it works on my machine’ and promote the importance of all users being in sync with the depot at all times.
We also place a lot of value in our build infrastructure’s capability to tell us when performance concerns start to arise. Performance issues are often a form of high-risk technical debt that unless immediately tended to will require destructive actions at the end of a project in order to get the performance back on track.
Whenever a performance issue is highlighted by builds generated via CI, we immediately pay attention to it as it is only through constant tending of performance throughout an application’s development lifetime that we can hope to deliver an app that runs within the target frame rate.
Always Estimate For The Worst Case
When someone asks us to commit to something, we tend to want to impress. When it comes to estimating the complexity of a task in sprint planning sessions, this often manifests as over-optimistic estimates.
This is a problem for a few reasons. It gives the production team an unrealistic view of the project’s schedule and predicted velocity. When tasks continually overrun, they lose trust in the estimates, making it less and less possible for them to scope/plan for the future with any degree of certainty.
It also is harmful to the individual giving the estimates. The negative feedback loop of continuously being late delivering on work can be super-stressful. In the worst-case scenario, it leads people to want to impress more by giving even more optimistic estimates, and so the cycle continues.
We combat this by promoting the idea of always estimating pessimistically: when everything goes wrong and all our assumptions prove false, what is the worst-case time frame for a task?
Obviously this leads to much longer predicted development periods, but it does solve our two main concerns. Production can be confident that work will complete on-time at the very latest, and by following the underpromise/overdeliver model, individuals have a much more positive feedback loop with their work.
Be Proactive
Many individuals often believe that they only have as many responsibilities as are allocated to them by their managers. However, the inverse is often true: management is often looking for those who are actively requesting to take on additional responsibilities, and then doing a great job with them.
There is never any harm in asking your manager if you can own some new aspect of your work that you feel you can do a good job with. We all want increased agency with our work, and the best way to get it is by finding opportunities to demonstrate your capability.
It’s also important not to be passive when working on a task. The very nature of our work is interdisciplinary and so dependency chains with other staff is to be expected.
Often the nature of those dependency chains is really best understood by the engineer working on the feature. Therefore, it is super-valuable for the engineer to identify those risks/dependencies early and chase them down! This helps reduce the overall risk of the work the programmer is undertaking. It also reduces the probability of getting blocked partway through development.
Atomic Commits
When tasked with building a new system, or extending a current system, the overall task may seem monolithic. So, naturally, we are inclined to decompose it to make sense of the work during sprint planning.
It is sometimes surprising to see that this same inclination is often not reflected in an individual’s commits. Many developers instinctively hoard all of their changes until the entire task is complete, and then commit the entire thing in one mega-changelist. This is a problem for several reasons.
By keeping everything local, engineers have no assurance that code they wrote one week ago when starting on a feature is still stable, and so their cognitive load is super-high. They need to keep all the plates spinning in their head whilst continuing to work on the feature.
Without committing to source control as they go, the individual is completely at the mercy of their local machine. If anything bad were to occur to their local environment whilst working on a feature, all of their work would be lost. Furthermore, if for whatever reason they need to take a leave of absence, the studio is effectively blind to the progress they have already made on a feature.
The utility of buddy checks negatively correlates strongly with the size of a commit. You can bet someone reviewing a 50-file-strong changelist is not going to provide the same level of forensic detail as they would with a 5-file changelist.
The alternative model is what we call the ‘atomic commit’ model. Every commit should completely describe one atomic part of a feature, no more and no less. And so a feature should be composed of a whole heap of atomic commits.
This approach helps to minimise cognitive load, as engineers do not need to worry about code they have already committed, since this has been reviewed and tested. It helps programmers structure their system, taking the same decomposition approach as they did to their tasks, and by charting out the next few commits, it can be a useful tool for planning the development of the system in an agile fashion. They are also able to then review the development of their feature over time via small easy to understand changes in the revision history of files(s).
It also increases confidence in the behaviour of the system overall, because at each stage we have the opportunity for CI to validate the code, and for the studio to review the work-in-progress feature in builds.
Always Learning
We work in an industry whose landscape is constantly changing and evolving. Naturally, this means that if we want to remain at the cutting edge (and we do!) then we have to accept that we will need to continually learn new things in order to remain relevant.
At Magnopus UK we manage this by supporting the development of the engineering team in a variety of ways. We have the usual 1:1s, as well as annual personal development plans tailored to the individual. We run internal workshops that run over a series of weeks/months to help individuals upskill in areas of interest, and for those areas where we do not have an in-house expert, we support external training too.
Communication Is As Important As Coding
Writing clear and understandable code is acknowledged by everyone in the industry as a fundamental part of the job, so it can seem a little strange that often communication in other forms is overlooked by engineers.
We recognise that as part of a multidisciplinary team, communication is a fundamental part of the job, and applies at all levels of seniority. We cannot afford to be siloed when we are all so dependent on one another.
What communication means at Magnopus UK covers quite the gamut, but broadly speaking, we expect the entire team to practice good communication by:
Writing technical documentation with the reader in mind.
Communicating technical concepts to non-technical members of staff in a relatable manner.
Practising empathy when talking to any other member of the staff; it’s only by understanding another’s position that we can identify solutions that address all parties’ concerns.
And yeah, clear and understandable code!
Code Should Always Be Written To Serve The End-user
Sometimes when working on a feature, it can be tempting to spend more time on the task just to make the code more beautiful. However, the question we should always be asking ourselves is ‘how does this benefit the user?’. If it doesn’t, then we really shouldn’t be wasting our energy on it.
The temptation most often comes up with refactoring and addressing technical debt, where we could go the extra mile and make the code that much better, but the whole thing about technical debt is that most of the time it is totally transparent to the end-user. There can be a very fine line between worthwhile debt reduction and time that could have been better spent working on new features.
As a decision, it tends to happen pretty frequently during development. Due to its high frequency, it’s a decision that often has to be made individually, so we codify it into one of our values to help clarify the mindset we believe results in the best user experiences.