#41 Step Four of Evolutionary Architecture: Focus on Complexity
Tackle complexity before it takes over your system. Early detection and action can prevent major rewrites and endless maintenance headaches.
Over the past few weeks, we have looked at keeping things simple, ensuring we can maintain our code, and planning for growth. Now it is time to tackle the last step: dealing with complexity.
Your app will probably get complex (or at least complicated), even if it doesn't get enormous traction. Like in real life - you start with a simple house, but then you add a garage, a garden, maybe a swimming pool. Before you know it, you have got a whole neighborhood.
The same happens with code. Maybe you start with a basic user login. Then you add password reset. Then social media login. Then two-factor auth. Then single-sign-on. It gets complicated.
Your business will also change - it always does. Take an insurance company. They start with car insurance: checking driving history, car value, and accident risk. Simple enough. Then they add home insurance. Now you need to handle property values, natural disaster risks, and security systems. Different rules, different checks. Next comes life insurance. Suddenly you are dealing with health records, age calculations, and beneficiary management.
Each type seems similar on the surface - it is all insurance, right? But the rules are completely different. Your code starts filling up with "if this is car insurance, check X, but if it is home insurance, check Y, and if it is life insurance, check Z.” Before you know it, you are drowning in conditional statements.
The infrastructure side also brings its own complexity. Imagine a startup app - one web server, one database. Works great at first. Then traffic grows. The app gets slow. Adding a cache helps with speed. A message broker handles the communication. One database can't keep up, so you add replicas. The web server maxes out, forcing you to add more servers and a load balancer. And so on. Suddenly your simple system turned into a distributed one. Now you are facing node coordination, data syncing across databases, and network failures. What used to be "it works on my machine" becomes "which of these 20 machines is causing the problem?"
Complexity builds up everywhere - new features, business rules, servers, databases. And the more complex your system gets, the harder it becomes to maintain it.
How can you address such problems?
In the case of infrastructure, keep an eye on your system from the start. Add monitoring tools early. When you add new pieces, make sure you can see how they are performing. Also, try to automate as much of your infrastructure as possible, already from the start. Write code to handle server setup, configuration, and deployment. That way, when your system grows from 2 components to 20, you won't lose track of what is running where or how it all fits together.
Without good automation and monitoring, you will get overwhelmed trying to manage everything by hand. One small mistake in server configuration, one forgotten setting, and your whole system can break in ways that take hours to debug.
As your code grows, simple entity-level logic isn't enough anymore. At first, putting behavior directly in entities worked fine. But when these need to work together, it might be time for another solution.
What you can do is create an aggregate that watches over related entities. Think of aggregates as rule enforcers - they keep all operations consistent and prevent unauthorized changes. Example? A Prescription aggregate makes sure only the prescribing doctor can cancel their prescriptions. Or a BindingContract aggregate allows (or disallows) to sign an Annex.
As your business changes, update your architecture to match. Keep refining the system’s boundaries - split them when they grow too big, merge them when it makes sense, and drop them when they are no longer needed. Here is an example coming from my book:
Private medical clinics that use our software also want to offer telemedicine. We extend the Patient Treatment bounded context to accommodate this change. It looks like it fits there – after all, it is just another treatment method.
However, with telemedicine, the patient does not need to come for an appointment, and there is no physical examination. Additionally, no follow-up appointment will be planned. We start wondering if this is a good idea, but the decision is to leave it as it is.
Three months later, clinics decide to offer specialized treatment. When a patient needs a consultation with an external specialist, they have to call the specialist and book an appointment.
Once a month, all specialists collaborating with the clinic send their invoices. Then, the clinic sends another invoice to the patient. As this is another treatment method, you decide to extend the Patient Treatment again, despite the process being entirely different from the initial state.
Problems arise. Maintenance requires increasing effort. More than one development team is needed. Too many people are working in the same area, and communication between them begins to fail. Terms start to be used in different contexts— Doctor refers to either an internal doctor or an external specialist. The former is on a contract of employment with the clinic, while the latter sends invoices to the clinic.
Ultimately, Patient Treatment becomes enormous, and you spot that the scope, rules, terms, and behaviors are completely different from what was initially defined.
The wise decision is to split it now into several new bounded contexts:
Telemedicine. This covers remote patient consultations and treatment. It includes virtual visits, digital communication, and remote monitoring of patients.
On-site Treatment. This represents the initial process of patient treatment where the patient visits the clinic, is examined, agrees to the treatment plan, and starts the treatment.
Specialized Treatment. This handles interactions with external specialists and ensures that the collaboration with them is streamlined and distinct from the internal operations
Managing complexity is an ongoing challenge, not a one-time fix. As your system evolves, keep watching for signs that your architecture needs to adapt. Whether it is infrastructure growing beyond your ability to manage it manually, entities that need new ways to enforce rules, or bounded contexts that no longer fit their original purpose - be ready to make changes.
Last thing to remember: what worked perfectly six months ago might need a complete redesign today, and that is perfectly normal in the life of a growing system.