This article is adapted from a team presentation I delivered at a previous job, with specific business details generalized for broader relevance. The goal of the presentation was to introduce the team to the concepts of Accidental Complexity, Domain Driven Design, Modular monoliths and Event-based communication.
Most applications follow a similar growth pattern at the beginning. The company’s first engineer picks a framework of their choice and begins coding. With the primary aim of launching an initial viable product, many stick to the default organization that the framework offers, which is often divided by technical components (‘helpers, ‘controllers’, ‘models’, etc).
A few thousand lines of code later, the codebase is likely to present some of these problematic traits (I’ve seen these cases over and over again):
- A large “god class” that contains too much data or functionality within it. You can recognize these classes because they often represent the core object of your system, such as a
Bookingclass in the travel industry or a
Policyclass in insurance.
- Inheritance everywhere. Since the “god” class is so big, it becomes tempting to create subclasses that inherit everything, tweaking only specific parts to suit the child class’s logic. In reality, Inheritance is one of the biggest sources of errors and diminished productivity, because a developer needs to always keep in mind what the parent class does when developing or interacting with a child class.
- Bloated controllers and models where business logic, database, request validation and ORM magic are jumbled together. This makes unit testing of business logic nearly impossible.
- The code is structured by technical components rather than by functionality, making it hard for developers to get an idea of the project’s features or locate specific functions.
Sources of complexity
As software matures, its complexity inherently increases. This is because simple, ’low hanging fruit’ features are implemented first. As time goes on, customers will demand more sophisticated use cases. Additionally, as the product grows in popularity, the demands for better performance, security, data portability, analytical tools and scalability increases. This type of complexity is inherent to all software is called Essential complexity.
In contrast, Accidental complexity of software refers to the complexity introduced by the design decisions while building software. Poor decisions (like the ones mentioned before), amplify the complexity of software in an accidental way, while a good architecture reduce complexity,
On a graph that represents the complexity increase over time in a codebase, we observe that unmanaged accidental complexity can skyrocket, leading to productivity problems. However, with continuous team effort, it can be kept in check.
Techniques to manage software complexity
Fortunately, there are a couple of techniques proven to effectively reduce complexity. These can be easily applied in new projects or refactored in the context of an existing codebase:
- Name things right: Code is meant to communicate with both computers and fellow developers, so descriptive class and variable names increase maintainability by exposing more clearly the intent of the code. Establishing a “Ubiquitous Language” —a set of unambiguous vocabulary shared by all members and stakeholders of a product team— early on reduces miscommunication between developers and business.
- Value Objects are essential: Unlike with primitive types, Value Objects represent concepts from the domain more explicitly. They can encapsulate its own validation and business logic, enhancing reusability. Additionally, they serve as implicit documentation, making the domain concepts clearer for developers.
- Modularize your application: Breaking code insto smaller units makes it more comprehensible. When all code related to a feature (including tests!) is grouped together with minimal external dependencies, expanding its functionality becomes simpler and carries less risk. Modules should still be able to communicate with each other, but they must do so only through predefined interfaces that dictate how the interaction takes place. In DDD, this concept is known as ‘Bounded contexts’.
- Reduced software complexity leads to quicker software delivery, benefiting the business.
- Accidental complexity arises due to:
- Misalignment between business and engineering views of the world.
- A poorly organized codebase.
- Techniques to minimize complexity:
- Choose descriptive names.
- Use Value Objects to repesent domain-specific concepts.
- Organize the codebase into independent modules with well-defined interfaces.