Microservice
Let’s begin our journey with a concept that has become ubiquitous in recent years - Microservice .
System Scaling
Scaling refers to the process of adjusting hardware resources of a system. For example:
- When the system experiences high traffic, additional resources must be allocated to maintain optimal performance.
- Conversely, if the system is underutilized, reducing resources can help lower costs.
In general, scaling can be categorized into two types: Vertical Scaling and Horizontal Scaling .
Vertical Scaling
Vertical Scaling , also known as Scaling Up, involves upgrading a server to improve its performance.
For example:
- If a server lacks memory, additional RAM can be installed.
- If a server operates slowly, upgrading its CPU (e.g., 1-core to 4-core) can enhance performance.
However, relying on a single server in a large system poses significant challenges:
- Hardware limitations: A server’s capacity cannot be expanded indefinitely.
- Single point of failure: If the sole server fails, the entire system may come to a halt.
Horizontal Scaling
Another method is Horizontal Scaling , also known as Scaling Out.
Instead of relying on a single server, Horizontal Scaling builds a system by combining multiple smaller servers and distributing the workload among them.
In this model, scaling means adjusting the number of servers rather than changing the resources of a single server.
For example, consider a system initially running on a server, S1. During a traffic spike, adding another server, S2, can help distribute the increased load instead of requiring an upgrade to S1.
This approach reduces the risk of a Single Point of Failure , since if one server fails, the others can continue operating.
However, Horizontal Scaling comes with its own trade-offs:
- Increased Complexity: Managing multiple machines is inherently more complex than managing a single one.
- Network Overhead: Distributed systems rely heavily on network communication, which can introduce latency, increase security risks, and create additional points of failure.
Distributed System
Horizontal Scaling is a fundamental principle behind Distributed System . Simply put, a distributed system is a set of machines that closely collaborate over a network to share resources.
Keep this concept in mind! a significant portion of this document will focus on the challenges and solutions associated with Distributed System .
Microservice
Now, let’s move the main part - Microservice .
Monolith Architecture
Traditionally, Monolith Architecture is the first choice of software engineering. In this model, all features are implemented within a single codebase and separated as modules. This approach provides simplicity and rapid initial development due to its centralized nature.
For example, a system with three modules might be structured as follows:
- Account.class
- Request.class
- SubscriptionPackage.class
However, as the system grows, its flexibility diminishes. In large systems maintained by multiple teams, sharing a single codebase can significantly slow down development because it requires tight coordination among teams. For example:
- Risk-Averse Changes: Teams may hesitate to modify shared components due to the risk of unintended consequences.
- Lock-Step Deployment: One team’s deployment may be delayed by issues in another team’s code.
To overcome these limitations, it is essential to minimize inter-team dependencies and enable teams to work independently and in parallel, with clearly defined responsibilities.
Microservice Architecture
Microservice is an architectural pattern that decomposes a system into smaller, independent services, each responsible for a specific function.
For example, a microservice architecture can split the previous system into three independent services, each owned by a different team.
- Account.class
- Request.class
- SubscriptionPackage.class
Ideally, microservices operate with a high degree of isolation, minimizing shared dependencies such as codebases, databases, and technology stacks.
This isolation empowers teams to manage their services with greater autonomy. Teams can choose technology stacks appropriate to their needs and independently deploy and test their services. Consequently, this autonomy can accelerate development cycles and enable teams to iterate more independently.
Microservices vs. Monoliths
Is a microservice architecture inherently superior to a monolithic one? The answer depends entirely on the context.
In a monolithic system, all modules reside within a single codebase and are typically deployed as a single unit. This centralized structure is often simpler to design, develop, test, and deploy, particularly for small to medium-sized projects. Modules can also communicate directly within the same process, resulting in lower communication overhead and latency.
By contrast, microservices are intentionally isolated and often communicate across a network. This distributed model introduces additional latency, creates more potential points of failure, and increases the complexity of monitoring, managing, and troubleshooting the system. Furthermore, maintaining strict service autonomy can lead to code duplication across services.
However, as organizations grow, especially to dozens or hundreds of developers, a monolithic architecture can become a development bottleneck. A large, shared codebase may increase coordination overhead, complicate deployments, and make it more difficult for teams to develop and release features independently.
Ultimately, microservices tend to provide their greatest benefits from an organizational and development perspective. They enable independent deployments, flexible scaling, and clearer service ownership. These advantages do not necessarily translate into better runtime characteristics, such as raw performance or reliability, because distributed systems introduce their own operational costs and failure modes.
Personally, I’m not a fan of Microservices, and I know many developers share this sentiment. Once data leaves my service and crosses the network, it becomes subject to a range of failures and uncertainties that can consume significant time and effort to diagnose.
That said, I find working within very large teams even more challenging. When something breaks, ownership can become unclear, and I may end up investigating problems that fall well outside my area of responsibility.
For me, that trade-off captures much of the appeal of microservices: they exchange some technical simplicity for stronger boundaries between teams and their responsibilities.
Microservice & Horizontal Scaling
A common misconception is that a monolithic system must run on a single server and rely on Vertical Scaling , while a microservice architecture inherently requires Horizontal Scaling .
In reality, architectural style and scaling strategy are separate concerns. Both monolithic and microservice systems can be scaled either vertically or horizontally, depending on their requirements and deployment environments.
Service Decoupling
Tight Coupling
A significant challenge in Microservice is tight coupling, where isolated services become overly dependent on one another and begin to behave more like components of a monolithic system.
For example, when a user completes a subscription purchase, the Subscription Service first retrieves the necessary account information from the Account Service.
After gathering these details, it then notifies the Account Service to update the account’s status accordingly.
Even though these services reside in separate codebases, they remain implicitly dependent on each other.
Changes to the Account Service, such as changes to its interfaces or logic,
can have unintended consequences for the Subscription Service,
requiring coordination and redeployment to prevent runtime errors and thereby limiting service autonomy:
- The more consumers the
Account Servicehas, the more coordination is required. - If the
Account Servicechanges frequently, dependent services must continually adapt to maintain system integrity.
While consolidating services into a single unit might seem like a straightforward solution, it risks creating a large service and reintroducing the very problems we sought to avoid with a monolithic system.
Coupling between services is, to some extent, unavoidable. Our goal should therefore be to minimize dependencies while keeping services as independent and loosely coupled as possible.
Loose Coupling
Loose Coupling involves minimizing dependencies between services so that changes in one service have little or no effect on others.
Services can be coupled in several ways, typically including:
Sequential Coupling
Sequential Coupling occurs when one service depends on another in a particular sequence of interactions.
For example, suppose the Subscription Service initially calls the Account Service to update an account’s status:
Later, if the Subscription Service also requires functionality for upgrading or
canceling subscription plans,
the Account Service must expose additional functions:
We can see that the Subscription Service must understand the internal responsibilities of the Account Service.
Whenever it requires additional behavior,
it effectively dictates how the Account Service must evolve.
As a result, the two services become tightly coupled,
increasing interdependency and reducing flexibility.
Topology Coupling
Topology Coupling refers to dependencies that arise from the arrangement and interconnection of services. When a service is added or removed, the overall topology changes, potentially affecting other services.
For example, suppose we introduce a Notification Service and a Fraud Detection Service.
The Subscription Service must then adapt to send subscription information to these new services:
Similarly, as new services are introduced or existing ones are removed, the Subscription Service must continually adapt to changes in the system topology.
However, for greater agility and maintainability, the burden of managing such changes should not rest with the Subscription Service.
Instead, responsibility for adapting to topology changes should belong to the individual components being added or removed.
Semantic Coupling
Semantic Coupling occurs when services depend on shared data structures and semantics.
For example, if the Subscription Service receives a response from the Account Service,
it must understand the structure and meaning of that response.
If the Account Service modifies the structure, the Subscription Service must be updated accordingly to prevent errors.
Services must agree on a common contract in order to interact with one another, so this form of dependency is difficult to avoid entirely.
Inversion of Control (IoC)
The Inversion of Control (IoC) principle can help reduce coupling between services.
Consider the previous example.
Suppose users query their status through the Account Service.
The Subscription Service actively controls updates to account status.
In other words, the Subscription Service depends on the Account Service.
Using
Inversion of Control
, we can attempt to invert this dependency.
Instead, the Account Service requests the current subscription from the Subscription Service and determines the account status itself.
The Subscription Service no longer needs to update account status directly or depend on the Account Service.
Instead, the Account Service becomes the initiator of the interaction.
However, simply inverting the direction of the call does not solve the underlying problem. The dependency, along with its associated drawbacks, still exists. Next, we’ll examine an indirect approach to implementing Inversion of Control using Messaging .
Messaging
The Inversion of Control principle can be implemented using Messaging . In this model, we introduce a message broker and three primary roles:
- Publishers publish messages.
- Broker stores and distributes messages.
- Consumers consume and process messages.
Applying Messaging to the previous coupling example:
- The
Subscription Servicepublishes subscription-related messages to the broker. - The
Account Servicelater consumes these messages and updates the corresponding accounts.
Following the
Inversion of Control
principle,
the Account Service now actively consumes and processes messages
instead of being directly invoked by another service.
As a result, neither the Subscription Service nor the Account Service directly depends on the other.
Decoupling With Messaging
Messaging helps reduce several forms of coupling:
- Sequential coupling: The
Account Serviceexposes only a minimal set of interfaces and adapts internally to handle different messages. At the same time, the responsibilities of theSubscription Serviceare reduced, giving it greater flexibility. For example, even if theAccount Servicetemporarily fails to process messages, theSubscription Servicecan continue operating without being directly disrupted.
- Topology coupling: Additional services, such as the
Notification ServiceandFraud Detection Service, can independently consume messages without requiring any changes to theSubscription Service.
Nevertheless, some dependencies still remain:
- Both services depend on
Messaging
. Fortunately, this dependency is relatively small and rarely problematic,
as Message Brokers typically expose simple
Publish()andConsume()interfaces that change infrequently. - Both publishers and consumers must adhere to the same message schema, resulting in Semantic Coupling.
In some situations, however, messaging introduces unnecessary overhead that may outweigh the benefits of decoupling:
- Indirect communication can result in higher latency, making messaging unsuitable for certain low-latency workloads.
- Asynchronous communication can lead to temporary inconsistencies, since changes are not immediately reflected across services.
- Debugging can become more challenging because failures occur asynchronously and may be harder to trace across services.
In summary, while coupling in a microservice architecture cannot be eliminated entirely, it can be reduced and managed more effectively through Messaging .