Software architecture is the high-level design of a software system that defines its components, their interactions and the principles that guide their development. A good software architecture should be simple, consistent, modular and scalable. It should also support the functional and non-functional requirements of the system, such as performance, security and reliability.
However, not all software architectures are good. Some of them are based on copying or imitating other architectures without understanding their rationale or suitability for the problem at hand. This is what I call cargo-cult software architecture.
The Layered cargo cult
Cargo-cult software architecture is a term inspired by the cargo cults of some Pacific islands during the first half of the 20th century. These cults emerged when the islanders observed the arrival of airplanes with cargo from foreign countries. They believed that by building replicas of runways, control towers and airplanes with bamboo and coconuts, they could attract more cargo from the sky. Of course, this did not work.
Similarly, some software developers blindly follow popular or trendy architectures without questioning their purpose or applicability for their own projects. They add layers upon layers of abstraction and indirection to their code, hoping that this will make it more robust or flexible. However, this often has the opposite effect.
One of the most common examples of cargo-cult software architecture is layered architecture. Layered architecture is a way of organizing code into separate levels of abstraction that depend on each other in a hierarchical manner. For example, a typical web application may have a presentation layer (frontend), an application layer (controller), a business layer (service), a data access layer (repository) and a data source layer (database).
Layered architecture has some benefits when used properly. It can help to separate concerns, promote reusability and testability and facilitate changes in one layer without affecting others. However, it also has some drawbacks when used excessively or inappropriately.
However, layered architecture also has many drawbacks: it introduces artificial boundaries and dependencies between layers that may not reflect the actual domain model or use cases; it adds overhead and latency due to multiple calls across layers; it creates boilerplate code for mapping and transforming data between layers; it obscures the flow of control and data within the system; it hinders refactoring and evolution due to rigid layering rules; it encourages anemic domain models that lack behavior and logic; it violates the principle of least astonishment by hiding important details behind abstractions.
In conclusion, while layered software design can be an effective way to organize complex systems, blindly following established patterns can lead to cargo-cult behavior and overly complex, difficult-to-maintain systems. Instead, software developers should approach the design process with a critical eye, evaluating the trade-offs of different design patterns and adapting to the needs of their specific use case. By doing so, they can create systems that are easier to maintain and that deliver more value to their users.
In my opinion, most layered software contains way too many layers, and the downsides of having too many layers are:
- Maintainability: The code becomes more complex and harder to understand as it is scattered across multiple files and classes. It also becomes more prone to errors as changes in one layer may have unexpected side effects on other layers.
- Readability: The code becomes less expressive and readable as it involves more boilerplate code and ceremony to pass data and logic between layers. It also becomes less consistent as different layers may use different naming conventions or coding styles.
- Understandability: The code becomes less clear and intuitive as it obscures the core functionality and business logic behind multiple levels of abstraction and indirection. It also becomes less coherent as different layers may have different responsibilities or assumptions about the system.
- Learnability: The code becomes more difficult to learn for new developers as it requires more knowledge and skills to master all the layers and their interactions. It also becomes more challenging to onboard new developers as they need more time and guidance to get familiar with the system.
These drawbacks lead to lower productivity and quality of software development. It makes development and bug fixing slow. Making small changes to data structures or logic often results in the "shotgun surgery" antipattern.
Shotgun surgery is an antipattern that occurs when a single change requires modifications in many places across the system due to high coupling between components. This makes the change tedious and error-prone as it increases the risk of introducing new bugs or breaking existing functionality.
So how can we avoid these problems? How can we design better software architectures that are not based on cargo-culting existing patterns without understanding their purpose or context? Here are some suggestions:
- Start with the domain model and use cases instead of predefined layers. Identify the core entities, value objects, aggregates, services, repositories, events etc. that capture the essence of your problem domain. Use domain-driven design principles to guide your modeling process.
- Choose an appropriate architecture based on your problem domain analysis. You should not blindly follow popular or trendy architectures without evaluating their pros and cons for your specific context. You should also consider alternative architectures that may offer better solutions for your challenges.
- Once you have chosen an architecture for your system, you should apply it consistently throughout your codebase. You should follow its conventions and guidelines to ensure coherence and compatibility between components. You should also document your architectural decisions and rationale to make them transparent and understandable for yourself and others.
- Apply YAGNI (You Ain't Gonna Need It) principle when adding abstractions or indirections. Don't introduce unnecessary complexity or flexibility unless you have a clear requirement or benefit for doing so. Prefer simple solutions over complex ones. Refactor your code when you encounter duplication or inconsistency rather than anticipating future changes.
- Review your code regularly with other developers using techniques such as pair programming, code reviews, and pull requests. Get feedback on your design choices, coding style, and naming conventions. Learn from others' experiences, perspectives, and opinions.
In conclusion, layered architecture is not inherently bad, but it can be misused or overused by developers who follow cargo-cult programming practices. We should be aware of the tradeoffs and pitfalls of this architectural style, and apply it judiciously and appropriately to our specific context and needs. We should also be open-minded and curious about alternative approaches that may offer better solutions to our problems. We should strive for simplicity, clarity, coherence, and elegance in our software architectures, as they are essential qualities for creating high-quality software systems.