Scalability Fundamentals

Architectural Patterns


Learning Objectives

  • You know of monolithic, microservice, and event-driven architectural patterns.
  • You understand the benefits and drawbacks of monolithic, microservice, and event-driven architectural patterns.

When designing scalable systems, there are decisions to be made about the architecture of the application. The architecture of an application defines how the components of the application are organized and how they interact with each other.

Here, we briefly discuss three key architectural patterns used in building web applications: monolithic architecture, microservices architecture, and event-driven architecture. The first two relate to the composition of an application, while third relates to how components of an application communicate with each other.

For code-level structure, all can use layered architecture, which is often the starting point for structuring application code as the codebase grows.

Monolithic Architecture

The term monolith refers to “an organized whole that acts as a single unified powerful or influential force” (Merriam-Webster), and the term monolithic architecture refers to an application that is a single coherent unit where all the code is in the same directory hierarchy and where the application is deployed as a single unit.

If the application uses a database, the database can be separate from the application, as shown in Fig. 1, but it can also be included within the application.

Fig. 1 -- Web application with a client, a web server, and a database (3-tier architecture).

Fig. 1 — Web application with a client, a web server, and a database (3-tier architecture).

To enhance the throughput or performance of a monolithic application, as mentioned in the chapter on defining scalability, one approach is to horizontally scale by increasing the number of servers and introducing a load balancer to distribute incoming requests across these servers (Fig. 2). In this setup, using monolithic architecture, each server runs the entire application, which means that the behavior is consistent regardless of which server handles the request.

Fig. 2 -- Increasing the number of servers and adding a load balancer to distribute incoming requests (4-tier architecture).

Fig. 2 — Increasing the number of servers and adding a load balancer to distribute incoming requests (4-tier architecture).

From a scalability standpoint, vertical scaling (adding more resources to a single server) is typically straightforward, while horizontal scaling (adding more servers) can be achieved by deploying multiple instances behind a load balancer, as illustrated above. Monolithic applications can be also easy to work with as the code is in a single location, the application is easy to test as a whole as it does not have to deal with inter-service communication, and the development environment is simpler.

However, monolithic architectures face scalability limitations as systems grow in complexity. Huge codebases can be challenging to maintain and to work with, even with sensible practices for organizing the code. Scaling specific functionality is also challenging, as the entire application needs to be scaled, and, when scaling horizontally, there may need to be a mechanism for sharing state between instances, which can introduce complexity.

Monolithic architecture is a good starting point for new projects. Martin Fowler advocates for a monolith-first approach, where development begins with a monolithic application and evolves into other architectures as needed.

Loading Exercise...

Microservice Architecture

Microservice architecture refers to building an application as a set of services that can each be developed and deployed separately and that communicate with each other using APIs to form a cohesive whole. Figure 3 illustrates a simple microservice architecture with two services: an authentication service and a product service, each with its own database. The user interface communicates with the services through an API gateway, which routes requests to the appropriate service.

Fig 3. — Microservice architecture consisting of two microservices, one for authentication and one for products. The microservices are behind an API gateway, which routes requests to the appropriate service. Each service has its own database.

Microservice architectures address some of the issues of monolithic architectures. Each service in a microservice architecture can be scaled independently based on demand. This allows scaling only those parts of the application that require more resources, which can be more cost-effective than scaling the entire application.

Similarly, individual microservices can be developed, tested, and deployed independently, speeding up the development process and making it easier to manage the codebase. If the application is divided into microservices, each service can also be monitored separately, and failures can be isolated more easily — if one service fails, the whole system does not necessarily go down.

Microservices also enable the use of different technologies and languages for different services, which can be beneficial when parts of the system have varying scalability needs or when teams working on specific microservices have different expertise.

On the other hand, dependencies between microservices can lead to increased complexity in development, testing, and deployment. A team working on a microservice needs to account for other services that use the service, avoid breaking changes, and maintain compatibility. When compared to monoliths, microservices also require more advanced monitoring, orchestration, and communication between services, which can add to the operational overhead of the system.

Implementing microservices often involves using containerization technologies like Docker and orchestration platforms such as Kubernetes to manage the deployment and scaling of individual services.

Microservices are said to come with a microservice premium, which is the increased cost of developing a system as a set of microservices compared to a monolith.

While monolithic architecture is recommended as the starting point for new projects, monolithic architectures can be broken down into microservices as the application grows and the system’s complexity increases. Microservices can be a good choice for large-scale applications with multiple teams working on different parts of the system and when parts of the system have varying scalability needs.

Loading Exercise...

Event-Driven Architecture

When designing microservice architectures, one of the questions is how to communicate between services. Services can, in principle, communicate directly with each other. This can, however, lead to tight coupling, making the system harder to maintain and scale.

Event-driven architecture is a design pattern focusing on producing and consuming events, providing a means for communicating between services without requiring services to be aware of each other. In other words, in an event-driven architecture, components responsible for creating events and components responsible for handling events are decoupled, and a mechanism is used for passing messages between the decoupled parts.

One way to implement an event-driven architecture in a web application is the use of the publish-subscribe pattern. Publish-subscribe is a messaging pattern that has three types of actors: message producers, message brokers, and message consumers. Producers create messages, which they send to brokers. Brokers receive messages and send them to consumers, decoupling producers and consumers so that they are not aware of each other.

The three types of actors are shown in Figure 4 below, which illustates a system that has two event consumers, one of which writes to a database and the other logs activity.

Fig 4. — Producers, brokers, and consumers. Producers produce messages, which are sent to brokers. Brokers then send the messages to consumers. Producers and consumers are not aware of each other.

Event-driven architecture is present in a wide variety of applications. For example, in user interfaces, events such as pressing a button are typically decoupled from the functionality that processes the button press. This can be implemented using event or action listeners, which are used to register functionality that should be executed in response to events.

Event-driven architectures and the publish-subscribe pattern provide the possibility of scaling producers, brokers, and consumers separately. For example, if producing a message is resource-intensive, one could create a large number of producers, each sending out messages when ready. Similarly, if a broker is under heavy load, additional brokers can be created, each responsible for specific types of events. Finally, if there are plenty of messages to be consumed, the number of consumers per consumer type can be scaled to match the demand.

At the core of the publish-subscribe pattern is asynchronous processing of information. Messages are created by producers, which send the messages to brokers. Brokers receive the messages and pass them forward to the consumers. This asynchronous processing is often enabled by message queues.

Message queues are services used for temporarily storing messages. They provide mechanisms to add messages to the queue and to retrieve messages from the queue.

Message queues can be used by brokers in the publish-subscribe pattern. In such cases, incoming messages from producers are temporarily stored in a message queue, which the broker processes by sending messages from the queue to the consumers.

Message queues enhance fault tolerance and scalability. Since messages are stored in the queue until they are retrieved, the failure of consumers does not lead to lost messages. In the event of a consumer failure, messages remain in the queue until the consumer can process them again. Similarly, during high load periods where producers generate more messages than consumers can handle, messages can accumulate in the queue and be processed when consumers have sufficient resources.

However, debugging and tracing errors in an event-driven architecture can be challenging due to the system’s asynchronous nature. As messages are passed between components, it can be difficult to follow the message flow. Ensuring data consistency is also more complex, as messages are handled asynchronously.

Loading Exercise...