At Monzo, we have a set of engineering principles which encapsulate what we value, what we expect our engineers to embody, as well as to provide a foundation for collaboration with peers when making decisions and navigating trade-offs.
We first wrote these in 2018, and recently invested in updating them to better reflect where we are now as an engineering organisation. What we value has evolved over time, and our principles should change to remain aligned with that – we don’t want these to be just words on a page, but a useful resource to guide decision making, inspire better systems, and be the foundation for a culture where engineers can thrive.
Here are our new principles:
🤏🏾 Make changes small, make them often
Small and incremental changes make it easier to spot bugs, are faster to review, and safer to roll back.
🙋🏻♀️ Unblock others whenever you can
If someone is stuck, helping them is an incredibly high-value use of your time: spreading skills and knowledge levels everyone up. If someone is waiting on you for a code review, feedback on a proposal, etc, prioritise that ahead of picking up something new yourself.
💖 Leave things better than you found them
Strive to improve things incrementally with each interaction – this could be any of our codebases, documentation, process, etc. Anyone can propose a change to any part of what we’re building, and you’re never stepping on anyone’s toes by trying to make things better.
💥 Optimise for impact to effort ratio
Don’t let perfect be the enemy of good — aim to make the most impactful changes and be okay with not solving everything in one go.
🐙 Take ownership of problems
Strive to proactively identify, analyse and resolve problems within your area: be the driver of change. This can occasionally mean going beyond your immediate role – for example, doing a little data analysis to support your case, reading regulation, or mocking up a basic design to convey an idea. Doing so is great, and can really accelerate progress.
🪄 Make technical investments that increase our velocity
We have lots of opportunities for this: automating something manual, fixing noisy alerts, reducing escalations, etc. If it’s a small investment (timebox to half a day), feel empowered to just go ahead and make it. If it’s bigger, assess whether or not the time spent resolving it will be less than what we’d normally spend doing it over the next 12 months. If so, it is very likely that it’s something we want to do.
🚪 Consider reversibility of decisions
We move quickly at Monzo, and our default should be to make good decisions at high velocity, rather than slow, perfect decisions. However, not all decisions are created equal. When you need to make a decision that is difficult to reverse or has long-lasting impact, take the time to think it through carefully.
🔒 Design systems to prevent misuse
We’re all responsible for the security of our systems, holding them to a high bar to protect the data and money our customers trust us with. Consider how attackers, external or internal, could exploit weaknesses and unhappy paths in your design and proactively mitigate them.
We prefer systems with secure defaults, leveraging existing solutions where possible. Interfaces should be difficult to misuse, and avoid unnecessary complexity which could leave them open to security issues or human error.
🌍 Think global first
As you design, build and test systems, think across products (personal, business, joint, etc) and markets. This doesn’t mean all features should be globally ready from the start, nor does it mean we can’t have product or market-specific features. However, adjusting copy is easy; changing a data model is not. Evaluate carefully which design choices you can make to be globally ready by default, and which can be deferred.
🌈 Extensible, not flexible systems
Build extensible systems that excel at specific tasks today and can be expanded later, rather than flexible overly generic systems designed to anticipate multiple future needs. Flexibility often leads to unnecessary complexity by pre-emptively solving hypothetical needs, whereas extensibility defers complexity until we need it. This aligns the design more closely with actual requirements, and minimises guesswork.
One caveat to this is that we would, for example, encourage adding a country code to a data model of a backend service, which might be deemed “flexible”. Doing so however would prevent a time consuming and expensive migration in the future, so do consider when adding a little flexibility comes at very low cost but high potential gain.
🧱 Consider tests as the foundations of our product
Tests allow us to build things faster and help prevent poor outcomes for customers and for Monzo. A strong set of tests gives us the ability to safely grow while ensuring speed of development, quality and customer satisfaction. When we build new systems, consider how best to test them as a part of its design. We’re all responsible for making sure we have adequate tests in place, providing confidence in the quality of what we are releasing.
⚡ Don’t optimise prematurely
If you can’t show with data it’s a bottleneck, don’t optimise it. Correctness and readability is nearly always more important than performance. Optimisation generally increases code complexity; only go after performance when you are sure a program works correctly and is running at a sufficiently large scale that the gains will be significant. As always, be pragmatic about this – don’t design something painful to extend or obviously expensive to run, just because we don’t have data to show that.
🧰 Technical debt is a useful tool
Technical debt, when managed thoughtfully, is a strategic tool that allows us to accelerate delivery. Rather than viewing it as a purely negative, we treat it as a deliberate investment that, like financial debt, requires careful consideration and planning for repayment. Left unchecked, the ‘interest payments’ on technical debt can severely hamper a squad’s long-term velocity.
One thing worth considering is the cost of delay. For example, if comparing a “hacky” fix vs a “proper” fix, consider whether the effort required for the “proper” fix will be the same in a year’s time. If it will be much harder to do the proper fix later (e.g. maybe it will need a migration or a lot more testing) then factor that in.
👾 Build systems to be read and debugged by humans
We solve difficult problems, and we should strive to do this in a simple, consistent and concise way. If code is hard to understand, it’s probably too complex. Systems we can’t reason about are especially prone to bugs, so we should keep things simple to avoid issues wherever possible. Default to, at a minimum, providing brief comments for high-level constructs like services and endpoints — in particular, capturing the ‘why’ can accelerate understanding for others. Assume that your reader doesn’t work in your squad and has no context on this system. No-one should need to dive into the implementation of a service to understand its external interface.
🧩 Pay attention to unexpected system behaviour
When a test or system exhibits unexpected behaviour, consider it an early warning for underlying problems and investigate. Proactive intervention can reveal gaps in design assumptions, unforeseen interactions, and prevent significant failures — ensuring system reliability and safety. In addition, it can be easy to fall into the risky trap of considering unexpected behaviour ‘normal’ and becoming accustomed to it.
Every deviation from expected behaviour is an opportunity to learn, grow, and build more resilient systems. We build financial products: the stakes for unexpected behaviour are higher for us.