Have you ever picked up a task that looked simple, only to discover that you needed to understand five other parts of the system before you could safely and confidently change anything?
That is what software complexity often feels like, where a small change requires a lot of understanding.
After more than two years working at Kraken Tech, I’ve seen our systems become more complex as we support more clients with different needs. Teams that manage this complexity well tend to make changes faster and have fewer incidents. Their systems are also more enjoyable to work on, which encourages engineers to keep improving them.
This has led me to think about software engineering through a particular question:
What constraints can we put in place to make the system easier to understand and safer to change?
This illustration from linear programming offers a useful way to think about constraints: they narrow the feasible region, or the set of possible solutions that satisfy the rules. In software, well-chosen constraints similarly narrow the approaches we can take, leaving fewer possibilities to understand while preserving the solutions we need.
Before looking at those constraints, it helps to understand where complexity comes from.
John Ousterhout’s A Philosophy of Software Design offers a useful way to understand complexity through its two main sources:
-
Dependencies: Changing one part of the system means you also need to understand or change other parts. For example, renaming a field might require changes to the database, API, user interface, and reports.
-
Obscurity: Important information is difficult to find or understand. For example, a function only works if another function runs first, but nothing makes that requirement clear.
Some complexity comes from the problem itself. If an application lets different users access different information, it needs rules about who can see what. We call this essential complexity.
Other complexity comes from how we build the solution. Perhaps those access rules are copied into several places, each with slightly different behaviour. Now developers need to find every copy and work out which rules apply. We call this accidental complexity, because it is not required to solve the original problem.
While we cannot remove complexity inherent in a problem, we can make choices that reduce the additional complexity our solution introduces. I believe constraints can be a powerful tool for curbing the growth of this accidental complexity.
Programming languages give us many ways to solve the same problem. That flexibility is useful: we can explore ideas and choose an approach that fits the situation. But imagine finding five different ways to do the same thing in our codebase. Before making a change, you now need to work out:
- How does each approach work?
- Why are they different?
- Which one should I use?
- Will choosing one cause problems elsewhere?
In a large codebase, developers (and now AI agents) can independently produce many different solutions to similar problems. Each solution may look reasonable on its own, but together they create more patterns to learn and more interactions to understand.
Constraints can help reduce this variation. A constraint is a limit on what we can do or how we can do it. Useful constraints remove decisions we should not need to make repeatedly and help prevent mistakes. They give us fewer things to figure out each time we work on the system.
Think of it like a shared kitchen. If everyone puts utensils wherever they prefer, each person’s choice might make sense to them. But everyone else has to spend time finding the utensils they need before they can cook. Agreeing that knives go in one drawer is a constraint. It removes your choice of where to put them, but it also removes everyone else’s need to search.
Good constraints remove noise and make good decisions clearer.
You probably already use constraints in your codebase, even if you don’t think of them that way. Here are some of them:
-
Types and type checking constrain the values our code can accept.
For example, declaring that a function expects a date makes that requirement explicit. A type checker can flag code that passes a string instead, helping developers catch the mistake before the code runs. -
Code formatters constrain how code looks.
They apply consistent spacing and layout. Developers can therefore spend less time making formatting decisions, and readers see more familiar looking code which reduces cognitive load in understanding it. -
Linters constrain common coding patterns.
They flag mistakes and patterns we have agreed to avoid. For example, a linter can flag wildcard imports such asfrom module import *in Python, encouraging developers to import specific names. This makes dependencies clearer and helps readers see where each name comes from. -
Software design constrains how we structure and extend the system.
A design pattern guides developers towards particular ways of solving a problem. For example, the Template Method pattern defines a workflow’s overall sequence while allowing developers to customise specific steps. This makes the intended extension points explicit and helps preserve required steps and their order. -
Programming paradigms constrain how we manage state and side effects.
For example, functional programming encourages immutable data and pure functions, which produce the same output for the same inputs without changing external state. This reduces dependencies on shared mutable state, making code easier to reason about and test. -
Tests check that our constraints hold.
For example, an application might require every command to have a unique name. A test can check the registered commands for duplicates, catching a naming conflict when someone adds a new one.
These constraints work at different levels: the data we pass around, the code we write, the patterns we follow, and the ways components interact.
Together, they make the codebase more opinionated. Common problems should have clear defaults and a small number of well established solutions. This allows developers to have fewer decisions to make, and readers have fewer possibilities to consider. These guardrails also give AI agents clearer boundaries and help catch mistakes in generated code.
Are there any other constraints you’ve been applying beyond those outlined above?

But… like with most things in life, you can overdo it! Too many constraints, or the wrong ones, can add complexity of their own. Constraints are powerful, but choosing the right constraints to implement still requires good judgment. A poorly chosen constraint can remove the necessary flexibility the system needs and makes a feature harder to implement. When developers are forced to repeatedly make awkward workarounds in order to implement legitimate requirements, there is a good chance the constraint may be adding to complexity rather than preventing it. Our jobs as engineers is to find the right balance in reducing unnecessary variation while preserving the required flexibility. Therefore, we need to judge each constraint by whether it makes the overall system easier to understand and safer to change, and revise it when it no longer does.
I would encourage you to try out thinking in constraints. Ask yourself:
What constraints can you impose to make something consistent, explicit, or impossible, so the next developer has less to figure out?
When familiar problems have familiar solutions, it allows our engineers to be more productive and be more confident in their changes. Reviews become easier, mistakes become easier to spot, and changes become safer. This allows us to focus our attention on solving the core problem, rather than untangling a complex solution.
You can start with one rule you keep explaining in code reviews. Could an interface, a linting rule, or an automated test make that rule part of the codebase? Every well-intentioned constraint is one less thing the next developer (or AI agent) has to remember.
Take a step back, and you’ll find you can apply this idea to many areas of life and business.
Limited resources push startups to prioritise and innovate. Time limits encourage focus. A regular exercise schedule removes a daily decision. Well placed constraints can give us focus, encourage creativity, and make good habits easier to maintain. The idea of leveraging constraints to get a better outcome doesn’t just apply to software engineering.