Introduction
In software development, adhering to fundamental system design and software architecture tenets is key for creating robust, scalable, and maintainable systems that can evolve. These tenets serve as the foundation of designing applications that can grow and scale. Such tenets guide architects and developers in structuring applications that are resilient, adaptable, and efficient. Understanding and implementing them can be the difference between systems that fail and systems that succeed.
In this article, I want to outline five such principles or tenets. These are by no means the only ones out there but they are among those that you will encounter and use most often.
It's also important to note that these are not architectural patterns as the latter are blueprints for specific use cases. There are dozens of such patterns that outline standard best practices for designing specific elements of your system and for solving common problems.
Architectural tenets, on the other hand, are more general ways of designing an application and there are many patterns that are based on these tenets. They are foundational to the software architecture and the role of the software architect.
For a great resource on system architecture patterns, visit microservices.io which is a great online resource with a comprehensive list of examples and use cases.
There is also a great summary of the most common patterns at https://www.redhat.com/architect/14-software-architecture-patterns.
Now, let's dive into the five systems design tenets that you came here for...
The Tenets
Modularity
Modularity in software architecture refers to the design approach where a system is divided into distinct components or modules, each with a specific responsibility.
Writing software in a modular way allows for easier substitution of components, simpler maintenance, and better reasoning about the software. These concepts also apply to software architecture as a whole as modularity scales from individual components to entire systems.
For instance, when choosing a messaging technology like Active MQ for an application, preparing for potential future changes, such as switching to Kafka, can be facilitated by
implementing an adapters abstraction layer. This layer isolates the interaction with the messaging mechanism from the business logic, making it simpler to replace the underlying
technology without impacting the entire application.
Loose-Coupling
Loose coupling involves minimizing dependencies between different components in your code, application, or ecosystem. This independence allows for modifications to any component with minimal impact on others. Consider two services. If one service exposes internal configuration parameters directly to the other, it tightly couples both of them. Changes in the one would require adjustments in the other - leading to code entanglement.
Another form of dependency comes from non-functional requirements. For instance, one service’s processing capacity is lower than the other service's, it becomes a bottleneck, tightly coupling both services at runtime.
To address these issues, prioritize patterns that reduce the coupling of components.
Separation of Concerns
The principle of Separation of Concerns (SoC) dictates that each component should have a limited scope of responsibility with minimal overlap with other components.
In other words, it means that every component has a limited scope of responsibility that is distinct from that of other components. This can apply to modules within an
application's code base. It can also apply to services within a distributed system.
Within a codebase, ideally, every function or module would be responsible for one thing and one thing only. However, if we are talking about microservices, typically, each microservice may be responsible for a number of things all of which fall within the domain context of that microservice.
Asynchronous Flows
In modern distributed applications, maintaining throughput and scalability during communication between components poses a significant challenge.
Consider two services: Service A, a user-facing RESTful API supporting 1000 TPS, and Service B, with limited capacity at 100 TPS. This imbalance causes Service B to fail in processing 90% of requests from Service A.
Note: TPS or Transactions Per Second is a measurement of how many requests a system can process in one second.
Transitioning from synchronous to asynchronous processing can help without impacting core business functionality. By introducing a message queue between the services, Service A sends requests to the queue instead of directly to Service B. Service B then processes requests from the queue at its own pace, alleviating the immediate failure issue.
Favour Stateless over Stateful
Stateless systems process each request independently without relying on previous interactions, enhancing scalability and resilience. In other words, what "stateless" means is simply that a system is not burdened with keeping the state between multiple requests. Any request that it receives is processed on its own, and it does not matter which instance of the system processes that request.
"Stateful" services, on the other hand, keep some sort of state in between requests. For example, an application instance that serves as the backend for a frontend (BFF), say for a website - might process all requests coming from a user's session in one particular backend instance and hold all information about that session within the local memory (state) of that
instance.
One typical example of a stateful system is a web application that maintains a user session and caches user information locally across multiple calls.
There are cases where statefulness is necessary. However, oftentimes, we can design our application in such a way so that large portions of its functionality are able to process independent requests without any knowledge of the relationship between them. This enables a whole host of use cases - first and foremost around application scalability.
Summary
The system design and architecture tenets we've discussed here are foundational to designing robust, scalable, evolutionary systems. They are not necessarily something that you need to implement to the same degree with every application that you build. That said, it helps to understand and be aware of these as you go about designing and implementing applications.
What's Next?
The architectural tenets, or principles, described above our only a small set of the tools in the software architect's tool belt. For more such tools, check out my in-depth guide - Unlocking the Career of Software Architect
Comments