Complexity

Software complexity 2

Headshot Bronislav Klučka on June 07, 2025

Continuing my previous article, we will discuss scale, support and accidental complexity, types you can manage, reduce and even remove.

Scale complexity

What makes big-ball-of-mud tempting is the ability to randomly call Article repository from Customer model or make database query for product price from invoice view. It’s fast but also this is how big-ball-of-mud is created, one coupling at a time.

We handle that by high cohesion and low coupling, modularization and separation of concerns. But that shifts the communication somewhere else where we have to moderate the communication.

Essential complexity cannot be eliminated.

For the purpose of this article, there are two main types of communication: one where you do not need a result (event) and one where you do need a result (query or command). This distinction is important. Whenever you don't need a result to proceed, use events. They're much easier to handle; just emit an event and continue.

Effectively managing queries requires an understanding of data retrieval speed and size. Commands introduce another layer of problems in the form of exceptions in the sequence of calls. Adding a network (e.g., microservices) introduces another set of issues.

So, first things first: Is it worth it? Yes, because although you create more complexity overall, each type of complexity (modules and communications) should be easier to understand and change as a separate entity.

What kind of solution we want here?

  • modules should not know where other modules are. They should emit events and query or command intermediaries.
  • modules should not depend on the internal behavior of other modules or the communication intermediaries.

Options here are:

  • event bus - modules publish events to the bus, modules subscribe to the bus
  • dumb pipes and smart endpoints - intermediaries should not contain business logic or understand the shape of a module's data. While it may be necessary for an intermediary to understand the type of event, query, or command that has been invoked, it should not understand its shape.
  • separation between the internal representation of data inside the module and the DTO used in API communication. This way, you can modify the internal representation of data to fit the required functionality without affecting the module API

Support complexity

So far, I've discussed the complexity required for business-level functionality and it's management. From now on, nothing that is discussed is necessary for the product to deliver its value.

We need to be able to manage and change every piece of code at any time in the future. We are responsible for it. Why am I saying that? Isn't that obvious? Well, it often doesn't seem that way. The less code you have, the easier things are. Now, to the point: It's not just about the code you write. The code I'm talking about also contains every single framework and library you include in your project. Once you do that, it's no longer third-party code; it's your code, and you are responsible for it.

But first, let's understand what support complexity is and why it exists. Support complexity most often relates to IoC (inversion of control). In simple terms, the IoC separates the application layer (application entry points, routing, middleware, etc.) from the business layer (the functions that your application provides to users).

We often use frameworks to handle IoC, but that choice often becomes problematic down the line. A few years later, we experience a substantial slowdown or even technical death, not because of the code we wrote, Rather, it is due to the code we have included. It's not just the application logic that depends on the framework; the business logic does too. When a new, incompatible version comes out, we're stuck. When the framework gets When the framework is abandoned, we are stuck. When a better option comes around, we are stuck.

Allow me to be controversial: Don't use those "big," "magic" frameworks. Angular, React, Vue, Symfony, NestJS, Nuxt, etc. They might make programming a bit faster for a few weeks, but they'll eventually slow you down. The price is too high in the long run. The same applies to ORMs (Doctrine, Mongoose, and Hibernate). Sooner or later, you'll find yourself working around the framework and modifying your code to fit it instead of the other way around.

These are general-purpose frameworks/ORMs that cannot handle the specific needs of your business case.

However, there must be some supporting code that simplifies management. What is the strategy here?

  • Explicit and strict separation of concerns: Your application logic and choice of architectural style should be separate from your business logic, and vice versa.
    • Imagine a user service or module that is self-contained (business logic, repository, model, etc.). It should not depend on whether it is in a layered architecture monolith or an event-driven, distributed microservice.
    • It shouldn't matter whether you use Symfony, Laravel, or your own framework.
  • I prefer a component-based approach, which involves specific, independent components for specific purposes, such as routers, middleware managers, and DSL translators, rather than a framework.
  • Your code should reflect your business logic. Never allow your business logic to depend on something you have no control over without an explicit strategy for how to change or replace it.

Of course, be rational here. You will depend on the runtime environment, which you have no control over (programming language, operating system, etc.), but you should limit it to the necessities.

What about something like a MIME builder, a component that can build email messages using the well-known MIME structure? Unless you are building a mail service, there is no point in writing code that has already been written to achieve the same result. Not counting security and performance, it's impossible to write a "better" version. The result must have a specific shape.

In the long term, your ability to control and shape the code as needed outweighs any initial speed boost. Always.

Accidental complexity

Accidental complexity serves no purpose. Essential complexity and scale complexity deliver business logic and management of that logic. Support complexity separates input/output from business logic, with input on one side and storage on the other. Accidental complexity is not beneficial.

Why would we do it? Why would we include it?

  • "We might need it in the future."
  • "Let's write it in a way that covers all cases, not just this one."
  • "Look how clever a programmer I am! I can write complicated code."

Just don't. Solve the problem you have in a way that fits the problem itself. Don't solve it in a way that might solve a bunch of problems in a future that may never come. As for the last point? Amateurs write complicated code. That's not a skill. Anyone can do it. What you need is smart code: simple, to the point, and easy to change.

There is an approach based in agile software development: "Making a decision at the last responsible moment." It is sometimes criticized as being wrong because it is interpreted as "don't think ahead." However, that is not what it means. What it means is: "Decide on a solution when you encounter a problem. Don't assume you have a solution to a problem you don't have."

What about security and performance? Those are problems you have from day one that are easily measurable and fixable.

What about scaling? If your application receives fewer than 100 visitors per day, then scaling isn't an issue. Why bother? Why create 20 microservices? Create a nice, modular monolith, and separate the modules when needed. You need to separate them based on actual needs, not assumptions.


I understand, by the way. I love programming, tinkering, creating gorgeous code and infrastructure, and playing with new, cool technology. But "Our highest priority is to satisfy the customer through early and continuous delivery of valuable software."

We use cookies and similar technologies, such as Google Analytics, to analyze website traffic. This allows us to understand how visitors interact with our site.

More info

This website uses Google Analytics, a web analytics service offered by Google. Google Analytics uses cookies to help us analyze how users interact with our site. The information generated by the cookie about your use of the website (including your IP address) will be transmitted to and stored by Google. We use this information to compile reports on website activity and to provide other services related to website and internet activity.

Analytics cookies help us improve our services. We do not use them for marketing or advertising purposes. We do not sell this data.