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 a system’s hardware resources. 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 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
Due to the limitations of Vertical Scaling , many opt for Horizontal Scaling (aka Scaling Out). Instead of relying on one server, Horizontal Scaling builds a system by combining multiple smaller servers using fewer resources.
For example, consider a system initially built with two servers.
Scaling in this model means increasing the number of servers rather than enhancing a
single server’s resources.
For example, during a traffic spike, adding new servers (e.g., Server 2
and Server 3
) can alleviate the load.
This approach allows for infinite resource scaling by provisioning separate machines. It also eliminates the risk of Single Point of Failure , since if one server fails, others can continue operating.
However, Horizontal Scaling comes with its own trade-offs:
- Increased Complexity: Managing multiple machines is inherently much more complex than a single one.
- Network Problems: Operating a distributed system requires extensive network communication, which may lead to reduced performance, security vulnerabilities, and potential network failures.
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
- Transaction.class
- Email.class
- PushNotification.class
However, as the system grows, its flexibility diminishes. In large systems maintained by multiple teams, sharing a single codebase can significantly slow development due to the need for tight coordination. For instance:
- Teams hesitating to modify shared parts due to the risk of unintended consequences.
- Even minor changes cause the entire system to be redeployed.
- Lock-step Deployment: One team’s readiness to deploy can be delayed by issues in another team’s code.
To overcome these limitations, it is essential to minimize inter-team dependencies and allow teams to work in parallel with clearly defined responsibilities.
Microservice Architecture
Microservice is an architectural pattern that decomposes a system into smaller, independent services, each handling a specific function.
For example, the microservice approach splits the previous system into three independent services and assigns them to different teams.
- Account.class
- Request.class
- Transaction.class
- Email.class
- PushNotification.class
Ideally, microservices operate in complete isolation, sharing no common dependencies such as codebase, databases, or specific technologies.
This fundamental isolation empowers teams to manage their services with full autonomy. It grants them the freedom to select their own technology stacks and to independently deploy and test their code. Consequently, this autonomy significantly speeds up development cycles and fosters greater agility.
Microservice & Monolith
Is a microservice architecture inherently superior to a monolithic one? The answer depends entirely on context.
In monolithic systems, all modules reside within a single codebase. This centralized structure makes it simpler to design, develop, and deploy, particularly for small to medium-sized projects. Modules communicate directly and efficiently (in-process), resulting in lower latency and higher performance.
By contrast, microservices are intentionally isolated and must interact across a network. This distributed model introduces added latency, creates more potential points of failure, and increases the complexity of monitoring, managing, and troubleshooting the system. Additionally, striving for complete autonomy in microservices can often result in code duplication across services.
However, as organizations scale, especially those with dozens or hundreds of developers; A monolithic architecture can become a bottleneck, hindering parallel development and making it difficult for teams to work independently.
Ultimately, microservices tend to provide the most value from a development perspective, enabling independent deployments, flexible scaling, and clearer team ownership, rather than offering benefits for runtime characteristics like raw performance or reliability.
To be honest, I’m not a fan of Microservice, and I know many developers share this sentiment. Once data leaves my service and crosses the network, it’s exposed to a host of unpredictable issues that can drain significant amounts of time and energy.
That said, I find working with very large teams even more challenging. When things break, it’s often unclear where to turn for help, and I frequently get dragged into problems that fall outside my area of responsibility.
Microservice & Horizontal Scaling
A common misconception is that a monolith system must reside on a single server using Vertical Scaling , while a microservice system always requires Horizontal Scaling .
In reality, the development model is separate from operational strategies. Both monolithic and microservice systems can be scaled either vertically or horizontally.
Service Decoupling
Tight Coupling
A significant challenge in Microservice is tight coupling, where isolated services become overly dependent on one another and 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 user’s account status accordingly.
Even though these services reside in separate codebases, they remain implicitly dependent.
Changes to the Account Service
, such as interfaces or logic,
can have unintended consequences for the Subscription Service
,
requiring coordination and redeployment to prevent runtime errors and limiting service autonomy:
- The more consumers the
Account Service
has, the more coordination needs to happen. - If the
Account Service
undergoes frequent changes, dependent services must constantly cope with it to maintain system integrity.
While consolidating services into a single unit might seem like a straightforward solution, it risks creating a large, monolithic service bringing back the very issues we sought to avoid.
Coupling between services is, to some extent, unavoidable. Our goal should be to minimize dependencies while ensuring that services remain 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 aspects, typically including:
Sequential Coupling
Sequential Coupling occurs when one service depends on another in a particular sequence.
For example, suppose the Subscription Service
initially calls the Account Service
to update subscriptions:
Later, if the Subscription Service
also requires functions for upgrading or
canceling subscription plans,
the Account Service
must expose additional functions:
We observe that the Subscription Service
grasps the inner logic of the Account Service
,
every time it needs something,
it dictates the Account Service
to accommodate that.
The services are tightly coupled with each other,
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 and can impact other services.
For example, suppose we add a Notification Service
and a Fraud Detection Service
,
and the Subscription Service
is then required to adapt to send payment receipts to them:
Similarly, as new services are introduced or existing ones are removed, the Subscription Service
must constantly adapt to these new topologies.
However, for greater agility and maintainability, the burden of managing such changes should not rest with the Subscription Service
.
Instead, the responsibility for handling dynamic topology adjustments should belong to the individual components being added or removed.
Semantic Coupling
Semantic Coupling occurs when services share the same data structures and semantics.
For example, if the Subscription Service
receives a response from the Account Service
,
it must understand the structure of that response.
If the Account Service
modifies the structure, it must notify the Subscription Service
to prevent errors.
Services need to agree on a common contract if they want to interact with each other, this dependency seems barely avoidable.
Inversion of Control (IoC)
The Inversion of Control (IoC) principle can help reduce coupling effectively.
Consider a car driving program:
- An
Engine
class controls the wheels. - A
Controller
is necessary to direct the engine.
Typically, the Controller
might actively invoke the Engine
.
In other words, the Controller
depends on the Engine
.
Using
Inversion of Control
, we try to invert the dependency.
Now, the Engine
drives the car by requesting the current direction from the Controller
;
That means it depends on the Controller
.
But purely inverting like this is no use, the dependency and its problems are still there. We’ll see an indirect approach to implement Inversion of Control called Messaging .
Messaging
The Inversion of Control principle can be implemented using Messaging . We essentially build an informative message broker with two primary associates:
- Publishers publish messages.
- Consumers consume and process messages.
Integrating Messaging into the first coupling example:
- The
Subscription Service
can publish account subscription messages to the broker. - The
Account Service
can later retrieve these messages to update the associated accounts.
By the
Inversion of Control
principle,
the Account Service
actively consumes and processes messages
rather than being directly invoked by another service.
In other words, its role is inverted,
from being called to a caller.
Decoupling With Messaging
Beneficially, Messaging moves us away from:
- Sequential coupling: The
Account Service
exposes only a minimal set of interfaces and adapts to handle various messages instead. Furthermore, the scope of theSubscription Service
is reduced, granting it more flexibility; Like so, even if theAccount Service
fails to process messages, theSubscription Service
continues to develop and deliver without disruption.
- Topology coupling: Additional services, such as
Notification Service
andFraud Detection Service
, can autonomously read messages without requiring any changes from theSubscription Service
.
Nevertheless, we still encounter some dependencies
- Both services depend on
Messaging
. Luckily, the dependency is minimized and barely problematic,
as Message Brokers expose only basic
Publish()
andConsume()
interfaces that rarely change. - Both the publisher and consumer adhere to the same message schema, which is Semantic Coupling.
Occasionally, messaging may result in an unnecessary overhead and outweigh the benefits of decoupling.
- The indirect communication model results in slower performance, making it unsuitable for low-latency workloads.
- Asynchronous communication can lead to temporary inconsistencies, since changes aren’t immediately reflected across services.
- Debugging may become more challenging as failures are asynchronous and harder to trace.
In summary, while coupling in a microservice architecture can’t be eliminated, they can be reduced and managed more effectively through Messaging .