Do not copy successful architectures
One common mistake in software architecture is blindly copying successful architectures without considering a project's unique needs and context. For example, many companies try to replicate the architecture of successful companies like Netflix without considering whether it is a good fit for their own needs.
It is important to remember that not all companies have the same scale or requirements as Netflix. Each architecture should be tailored to the specific goals and requirements of the project. Blindly copying successful architectures can lead to unnecessary complexity and inefficiency.
Relevance
In architectural design, there is no universal solution. The concept of a one-size-fits-all approach is ineffective. The most effective software solutions emerge from solving actual problems rather than striving toward an idealized vision.
What is essential for companies are robust, simple, and predictable solutions that are easy to adapt, develop, and maintain. It's akin to building cars: it wouldn't be practical to use every available piece of engineering without considering cost, service, time to market, and the real needs of users, such as safety and resilience.
Business needs should be at the heart of architectural considerations. An architect must deeply understand the business's context, vision, strategic plans, and history. This deep understanding should then be reflected in the system's architecture. The architecture should narrate the story of the business, revealing not only how it operates but also why it operates that way.
Simplicity
Keep it simple. Avoid solving problems that don't exist yet. Be aware of your scale, your team's capabilities, and their experience. Focus on your unique business niche, and keep your tech stack simple enough.
When deciding between mature and cutting-edge technologies, weigh factors like the project's criticality and the availability of support. While established technologies are generally safer and minimize risks, sometimes new technologies are necessary. In such instances, architects should make well-informed decisions based on the project's specific requirements.
Don't aim to build for millions right from the start. Many successful companies, such as Amazon and eBay, have rewritten their systems several times over the years. Companies that overcomplicate their systems to accommodate customers they don’t yet have often remain unknown—they fail in the market. Identify your strengths and leverage them. Solving only the problems you really have lowers the product’s time to market.
Microservices may not be necessary at the very beginning. The primary purpose behind microservices is independent deployment, and any other benefits are often just side effects rather than the intended goal.
Microservices should only be considered when there's no other viable option. They are neither simple nor inexpensive and require significant operational effort to maintain. They also add considerable complexity just to make independent deployment feasible.
Increasingly, companies are moving away from microservices due to cost and complexity. Modular monolith is a more than often sufficient approach that is easier to enhance, evolve, and refactor. When built reasonably, it is quite simple to transform into full-fledged microservices.
Evolution
You and your team will gather more and more knowledge over time. It is impossible to forecast all the challenges you are going to encounter. Embrace that uncertainty and partial knowledge. It's normal for architecture to be imperfect and to evolve. Architecture is a dynamic, not static, process that requires an iterative and incremental approach. Understand that your knowledge is always incomplete; every decision is potentially temporary, awaiting new insights. Embrace this as an ongoing process.
There will always be challenges and room for improvement. Do not aim for a final state where everything is complete, as it is unattainable. Instead, grow alongside the business, acknowledging that you can't know all the answers upfront.
Defer decisions when necessary. In fact, it is all about deferring any decision as long as possible. If you are unsure what the final decision may be, design it in a way that enables you to change your mind easily.
Avoid future-proofing your architecture, as it can divert attention from current problems. Predicting the future is not feasible, and attempts to do so can be detrimental. Focus on the present issue, thinking only one step ahead, not ten. Building universal solutions only adds complexity and rarely succeeds.
Continual learning and experience, both within and outside the organization, are vital for developing your skills and improving your capabilities.
Making informed architectural decisions based on understanding and experience is crucial. An iterative and incremental approach provides flexibility and the ability to rectify mistakes. Start with the best guess based on current knowledge and refine the architecture as new information becomes available. It's important to separate the well-understood aspects of the system from those that are not.
Allow yourself the freedom to make mistakes and learn from them. This discovery process is crucial in understanding constraints and dependencies in architecture.
Constraints
Every choice in architecture has its trade-offs. When you say 'yes' to something, you're inherently saying 'no' to something else. This concept highlights that nothing comes without a cost.
Additionally, every action, including choosing not to act, has consequences. It's important to realize that nothing in architecture exists in isolation; everything interacts with and is influenced by its surrounding context. I write more about that here.
Developers often search for the perfect solution to a problem, but such a solution rarely exists. Prioritizing certain values inevitably means compromising on others. This reality underlines the importance of knowing what to focus on, as specific solutions tailored to a particular problem often provide more value than generic ones.
Recognize your knowledge's limitations to avoid overconfidence and the issues it can cause.
Every decision and technology carries a cost. There is no such thing as a free solution. Even striving for independence from a specific tool or technology comes with a price, often adding complexity and diminishing the clarity and readability of the solution.
Modularization
Modularization is a fundamental concept in software development, emphasizing well-defined boundaries, information hiding, encapsulation of business logic, and data ownership. By using simple components, we can build increasingly complex systems. It stands as the primary method to manage complexity and cognitive overload.
When building modules, it's essential to find the right size. A too-fine-grained breakdown can lead to coordination and cooperation issues, managing dependencies, and knowledge and data sharing challenges.
The goal is to build systems that work for current needs, with the flexibility to split or merge modules in the future without impacting other parts of the system. This approach is often easier to implement in a monolithic architecture.
While complex systems may seem appealing, they often bring about complications. Simpler systems, on the other hand, can be scaled up to create more versatile and robust architectures.
The focus should not just be on the technology but also on semantic boundaries—defining clear language, responsibilities, and the flow of data and control. Modules should aim for autonomy and manage the data they require.
However, it's important to remember that modularization has no one-size-fits-all solution. It’s a process that enriches the system's language and enables meaningful discussions about it.
Perspective
The core idea behind a module is to hide as many details as possible from external view. Everything a module exposes constitutes its public API, which becomes challenging to change once other modules depend on it.
Carefully shaping these contracts and hiding decisions within the module allows for flexibility in design and assumptions, facilitating system evolution through refactoring.
A module’s strength lies in its internal invisibility from the outside, interacting solely through its public API. This design allows internal changes without affecting other modules, preserving the module's boundaries.
This modular approach also influences how we perceive and discuss these components. Defining a clear public interface for each module clarifies its role, responsibilities, and data ownership. Designing modules with their contracts in mind results in a system where components are less interdependent and more coherent.
Maintaining a system's architecture requires understanding it at various levels, exemplified by the C4 model. It's important to recognize the cooperating components at each level and the level to which they belong. Mixing different levels without awareness can be problematic.
Understanding a system's relationships and levels is crucial for making informed architectural decisions.
Context
Recognizing that a system extends beyond mere code is crucial. It encompasses a variety of practices and elements, including deployment strategies, documentation, continuous integration and deployment (CI/CD), organizational culture, communication, and testing.
These components are critical to the success and functionality of any software project. Implementing advanced techniques like canary deployments and feature flags is also essential for facilitating low-risk deployments. Additionally, having meaningful production logs and metrics is indispensable for making informed decisions.
A key factor in managing high-performing systems is the need for quick and high-quality feedback. Achieving this requires a significant investment in automated testing. Automated testing is not just about speeding up feedback and enhancing code quality; it also serves as a form of executable documentation. This type of documentation proves invaluable in understanding the existing code, simplifying modifications, and improving system design.
While code can serve as documentation, it should not be the sole source. Code often misses the wider context and rationale behind design choices. Documenting and tracking the reasons behind these decisions can be cumbersome.
Architecture Decision Records (ADRs) offer a solution to this challenge. ADRs provide a lightweight, easily accessible way to document and communicate the rationale behind architectural choices. They are crucial for keeping a clear, historical context of decisions, ensuring that the reasoning behind them is understood and retained throughout the project's lifecycle.
Summary
It's essential to observe your environment and surroundings continuously, stay informed about emerging technologies and approaches, and maintain a learning mindset. However, a healthy skepticism towards new trends is advisable. Always prioritize the relevance of the software to the business it supports. The true measure of a software's greatness is how well it aligns with and supports the business objectives it is designed to serve.