Introduction

Microservices is an architectural approach that aims to develop scalable, efficient, and reliable software.

In this article, we'll discuss:

  • Concept of microservices and common misconceptions associated with them.
  • Benefits and disadvantages of applying this architectural pattern.
  • Primary design considerations and guidelines using a real-world example.

Please keep in mind that the topic of microservices is extensive. Covering everything from A to Z in this article would make it excessively lengthy, which goes against the goal of the "In Simple Terms" series. Instead, this article starts with essential theory and concludes with a straightforward example to enhance understanding. If you find it a bit challenging at first, don't get discouraged!

Concept

In this section, we'll be exploring the definitions that will be utilized throughout the article, along with the necessary theory associated with the architecture.

Please do note that we're using "services" and "microservices" interchangeably.

Microservices

Microservices is a software architecture approach where an application is built as a collection of autonomous services, each responsible for implementing specific functionality of a well-defined context (e.g.: sending e-mails, handling notifications, handling user management & authentication) within a broader system. These services communicate through APIs and can be developed, deployed, and scaled independently. This approach promotes modularity, scalability, and maintenance, but also introduces complexity and challenges in managing service communication and data consistency, as well as monitoring and debugging.

The key characteristic of each individual microservice is isolation - without which none of the above benefits can be achieved. This should become clear once you reach Design & Guidelines.

Here's a simplified overview of a sample system built according to this architecture:

Context

A context of a microservice is the scope of functionality that the microservice implements. The size of this scope can vary significantly, ranging from single functions to entire domains of the system.

The context of your microservice can effectively be something as simple as a SendEmail function, or something as robust as the entire user authentication logic.

System

The term "system" refers to an application or platform that consists of multiple components that work together to fulfill a common goal (e.g.: providing backend for a forum). This includes all microservices, message queues, databases, and any other dependency that the system requires.

Misconceptions

As mentioned in the introduction, the topic of microservices is extensive. This has led to various misconceptions spreading like a wildfire.

In this section, we'll try to debunk at least a few of them.

  1. Microservices Must Be Small
    Despite what purists may suggest, and even though the term 'microservices' includes 'micro', the size of a service primarily depends on the functionality that the context defines and the precision of scalability needed. The goal isn't for it to be as small as possible, but rather as limited to a single context as possible.
  2. Microservices Make Monolithic Architecture Obsolete
    Microservices aren't a one-size-fits-all solution. Monolithic architectures are still a better choice for smaller applications, tightly integrated codebases, and systems that don't require immediate and precise scaling.
  3. Microservices Must Be Fully Decentralized
    While microservices focus on independence and modularity, it's crucial to remember that they work together toward a shared goal spanning multiple services. Utilizing shared tools for monitoring, communication management, and node management can significantly benefit the entire system. Imagine a situation where one service receives incorrect data - without a central logging system, you'd need to inspect each service's individual log to identify where the data corruption occurs - which is inefficient, to say the least.
  4. Microservices Must Use Tech-Agnostic Protocols And Formats
    While using technology-agnostic protocols and formats is usually beneficial, some companies may have proprietary solutions tailored to their needs. Using them is perfectly acceptable.
  5. Microservices Guarantee Better Performance
    To put it simply, microservices come with an overhead of additional storage reads and writes, cross-service communication, and whatever expenses dependencies like frameworks or libraries bring. If your system is built according to the Microservices Architecture and it runs only 1 node of each service, it could very well just be a monolithic application.

Pros & Cons

As mentioned previously, Microservices Architecture is not a panacea. It comes with it's own set of benefits and disadvantages - and these will be covered in this section.

Benefits

  • Resource & Cost Management
    Each service can be scaled independently, allowing you to have varying quantities of instances according to your needs and resources available on the host machine(s). This isn't possible with monolithic architectures, as you can only create more instances of the entire application, rather than individual parts of it. This means you'll have to buy another host machine to continue scaling, and you'll be wasting whatever resources were left and couldn't be assigned.

  • Development & Deployment
    The key characteristic of a microservice (singular) is isolation, which means that the service can be independently developed and deployed - without affecting other services in the system. This can significantly improve team autonomy, and thus speed up development, testing, prototyping, and experimentation, as well as monitoring and maintenance.

  • Resilience
    Microservices are designed to enhance resilience. Unlike monolithic architectures, failures in one service don't propagate to other services, preventing a system-wide collapse.

  • Interoperability
    Microservices promote interoperability. Each service exposes dedicated APIs that utilize technology-agnostic protocols and formats. As a result, these APIs can be utilized by any entity, regardless of technologies used.

Disadvantages

  • Resource Overhead
    Each service comes with its own separate copy of code, frameworks, libraries, and other dependencies. Additionally, each instance of that service needs to run a copy as well. All of that adds up to quite a significant toll on resource consumption from the get-go.

    Services have to communicate with each other to accomplish common tasks, which puts a notable strain on the network - strain rarely found in monolithic applications.

  • Complexity
    Complexity is the key consideration to make when deciding whether Microservices are the right fit for us.

    The benefits speak for themselves, but there are certain aspects that can't be skipped, such as:

    • Microservices having to communicate and share data via common conduits like APIs, Message Queues, and Service Buses - if not handled properly, this can introduce SPoF (single-point-of-failure) to the system or lead to data inconsistencies.
    • Changes to services often result in cascade changes to services using them - deployments must be synchronized accordingly.
    • Testing services is significantly harder due to dependencies on other services. Moreover, mocking becomes increasingly challenging.
    • Managing security and access control is significantly more complex.
    • Monitoring and tracing the flow of data between services can be complex, making it crucial to implement observability.

    Many of the mentioned complexities can be significantly reduced through the use of specific design approaches and dedicated tools. More information on these strategies can be found in the Design & Guidelines section.

  • Learning Curve & Initial Setup
    Microservices Architecture isn't as intuitive as certain monolithic architectures. Developers need to understand the trade-offs of this approach and how to strike a balance. Additionally, establishing the right infrastructure and tooling for microservices can be time-consuming.

Design & Guidelines

In the final section, we will present a simplified real-world example. We will break down each component, discuss why it was included, and highlight the value it brings. This is to get you started, showing you one of many ways to implement things in this architecture, issues you may run into, and good-to-know theory.

The example is an implementation of the backend for a fictional Forum called "ObeyForum".

Overview - System

This is the top-most view of the system. We're not delving into services just yet.

User
Self-explanatory. The user that interacts with our backend via HTTP requests.

Nginx
An HTTP server that handles all incoming requests (gateway). Implements reverse proxy and load balancing, both of which have to be put in place to take full advantage of this architecture.

Reverse Proxy
A practice in which a middleman server (e.g.: Nginx) forwards requests and responses. This allows for a single point of entry (gateway), compression, SSL termination, improved security, and more. In our scenario, a reverse proxy is needed to forward requests to otherwise private APIs, as well as to consolidate all service APIs under a single URL: api.obeyforum.com/{service} for easier consumption, e.g.: api.obeyforum.com/content.

Load-Balancing
A technique in which the load is distributed evenly among processing units. This allows for better performance, resource utilization, scalability, and reduces the possibility of an overload. In our scenario, HTTP requests get spread between instances of services.

Docker
Hosts containers of our services along with their dependencies. For in-depth information on this particular tool, refer to Docker in Simple Terms - Introduction.

Orchestration
Process of management and coordination of multiple containers that work together as part of an application. Orchestration tools help automate deployment, scaling, and management to ensure that the application runs smoothly and efficiently. In this scenario, we've omitted name of the orchestration tool used alongside Docker, as it's an unimportant detail that would cloud the process of learning.

Services
A placeholder component for all microservices. This will be explained in-depth later in this article. In the overview, it is used to mark where microservices reside in our system.

Message Queue
A communication channel between services, it allows them to send and receive messages, ensuring that information is reliably transmitted even when certain components are busy or unavailable. A crucial part of any system built according to Microservices Architecture. CQRS is implemented as a welcome side-effect.

Message Queues usually support one or more messaging models, with the most common ones being point-to-point and publish/subscribe.

Point-To-Point Communication
The point-to-point messaging model is a communication pattern where messages are sent from a single sender to a specific recipient (e.g., service-to-service). In this model, there are usually two types of messages: fire-and-forget (often called "commands"), and request/reply (usually called "requests").

Publish/Subscribe Communication
The publish/subscribe (or "pub/sub") messaging model is a communication pattern in which multiple recipients subscribe to specific topics (often called "notifications" or "events") and get notified whenever message on specific topic is published. This pattern is especially useful in achieving highly decoupled systems, as we can have potentially infinite quantities of publishers and subscribers, completely unaware of each other.

CQRS (Command Query Responsibility Segregation)
CQRS, or Command Query Responsibility Segregation, is an approach that splits data modification (commands) from data retrieval in a system (queries). This separation allows optimizing each process independently for better performance and scalability. Message Queues implement this pattern thanks to P2P communication.

Overview - Services

By now, you should have a vague idea of how the system works. The only component missing is understanding how services that implement our business logic operate.

Let's take a look at the overview of our services and their dependencies.

As you can see in this diagram, the entire communication is going through the message queue (RabbitMQ). This is not to say that there is no point-to-point communication. On the contrary, our cross-service communication should involve both point-to-point (P2P) and publish/subscribe (Pub/Sub) communication patterns - whichever is more appropriate in a specific case.

Additionally, as is apparent from this diagram - each service has a separate cache (Redis) and database (Postgres). Having separate databases and caches (if any) is a requirement to prevent SPoF (single-point-of-failure), concurrency and scalability issues. Data is increasingly harder to move, the more services use it.

The usage of caching mechanism to store replicas of data from other services is an approach promoting availability mentioned in CAP Theorem. The opposite would be making constant requests to other services whenever fresh data is needed - this puts more strain on the network, but also improves accuracy in services that benefit from it.

CAP Theorem
The CAP theorem, briefly, states that in a distributed system, you can prioritize at most two out of three attributes: Consistency, Availability, and Partition Tolerance. You have to make trade-offs between these attributes when designing and operating distributed systems. The primary trade-off our approach faces is explained by the Eventual Consistency.

Eventual Consistency
Eventual consistency, in short, means that after a certain period of time, all replicas or copies of data in a distributed system will reach the same state or value. While there might be temporary variations due to network delays or system failures, the system will eventually adjust these differences and become consistent. It's a concept often applied in distributed databases and systems to balance availability and data accuracy.

The Flow (Business Logic)

Finally, in this chapter, we'll discuss what each service in our system does, how it does it, and why it's implemented the way it is.

MailingService
The service responsible for sending e-mails to recipients.

  • The service accepts a single command called MailingSendEmailCommand.

  • The service requires no database and no cache. Commands sent to it are stored on the message queue and won't disappear until the command is fulfilled - which provides persistence needs for the service.

It's a simple service. However, sending e-mails comes with a rather notable delay, so it's a perfect service to fill leftover resources with.

AlertService
The service responsible for storing various alerts and notifications, both of which are stored in the cache.

  • The service provides an API for fetching alerts and marking them as read, enabling frontend consumption.

  • The service stores replicas of certain user data (IDs and nicknames) obtained from the AuthService in a cache. The replicas are fetched via AuthGetUserByIdRequest when user receives their first alert, and will update only when AuthUserChangedNotification is emitted by the AuthService.

  • The service triggers an MailingSendEmailCommand to inform users of important alerts via email.

  • The service subscribes to following notifications to generate alerts:

    • ContentThreadLockedNotification
    • ContentThreadRepliedNotification
    • ContentPostRepliedNotification
    • AuthUserBannedNotification

AuthService
A robust service handling both authentication and authorization. It oversees users, bans, ranks and permissions which are stored in the database.

  • The service exposes API that allows sign-up, sign-in, change nickname, change password, and recover password functionality to regular users and admins, so it can be consumed by the frontend.

  • The service exposes API that allows ban, unban, grant role, revoke role, grant permission, revoke permission functionality to admins, so it can be consumed by the frontend.

  • The service stores session tokens in a cache.

  • Sign-up, change password and recover password functionalities emit MailingSendEmailCommand with links used in finalizing these tasks.

  • The service emits following messages:

    • AuthUserBannedNotification
    • AuthUserChangedNotification
    • AuthUserSignedUpNotification
  • The service handles following messages:

    • AuthGetUserByIdRequest

ContentService
The service responsible for managing the forum's content. It oversees categories, sections, threads and posts, and stores them in a database.

  • The service exposes API so CRUD (create, read, update, delete) can be performed on the underlying entities.

  • The service stores replicas of certain user data (IDs, nicknames, ranks and permissions) obtained from the AuthService in a cache. The replicas are fetched via AuthGetUserByIdRequest when user publishes their first thread or a post, and will update only when AuthUserChangedNotification is emitted by the AuthService.

  • The service emits the following notifications:

    • ContentThreadLockedNotification
    • ContentThreadRepliedNotification
    • ContentPostRepliedNotification

Conclusion

In conclusion, the adoption of Microservices Architecture has transformed the way modern software systems are designed, developed, and deployed. The principles of modularity, isolation, and scalability bring undeniable advantages to the table, although not without trade-offs.

The Microservices Architecture is simple yet complex. Paradoxically, its simplicity lies in individual services and their communication with their nearest neighbors. However, learning to think in this highly unintuitive way can be challenging, even for seasoned engineers.

Understanding the theory behind Microservices is of utmost importance, as it helps you make informed choices during the implementation phase. Unfortunately, not everything could be covered in this article.

Hopefully, you can find this read useful. There's no telling when this extensive article will be broken into simpler, separate parts, but stay tuned!