The Zen of Microservices (2) : Monolith vs Microservice
Sample Monolithic Architecture
The monolithic architecture diagram depicts a well-designed, modular application. It has several well-defined internal modules. It interacts with external systems via adapters that separate the core application logic from infrastructure details like transport and persistence.
All calls between application modules are internal method calls
Deployment is a single event involving bundling the entire application
What is Microservice Architecture (MSA)?
Independent services vs Modules
Unlike monoliths, which are typically built out of modules, microservice architecture is built on independent services that are split out by business functions, with a share-nothing architecture.
- Each service performs a well-defined business function exposed via a public API contract
- A service encapsulates that function and therefore does not share it's data or data models with other services Each service maybe changed, deployed and scaled independently
Best Practices: Distributed vs. Monolithic
Microservice architecture places high value on independence and resilience which means that best practices from monolithic applications do not always translate over to MSA.
Some key examples:
Single Source of Truth (SSoT): Monolithic applications strive to maintain a single source of truth for each entity in it's data model. In MSA every single source of truth is a potential single point of failure (SPoF) so this is avoided. Key information is cached and redundantly persisted in order to facilitate higher availability and resilience.
Don't Repeat Yourself (DRY): The DRY principle which is useful for code reuse within a monolith is often relaxed in MSA. This is because code reuse is a form of coupling and MSA places higher priority on service independence. For this reason, replicating code in various microservices maybe preferable to being coupled to a shared library.
Unified Data Model: Monolithic applications often have a comprehensive, unified data model. Essentially this means that every instance of an Entity, e.g. Customer, will be uniform within the application. In MSA, this is considered harmful because it introduces data coupling. Each m/s encapsulates a business function and should be free to model entities (perhaps just a reference to CustomerId) within itself without affecting external services. This facilitates agility, independence and reduces data integrity errors across the system.
Message-based communication vs Method calls:
Monolithic applications pass data via method calls to internal modules or libraries with practically zero latency. Typically, large, complex data structures maybe passed around with little to no impact on system performance. In contrast, message-passing is the primary means of communication in MSA. A large part of processing centers around publishing, consuming, parsing, tracking, serializing and deserializing messages. The choice of message format (HTTP over JSON, RPC, XML, ProtoBuf etc) and their size and complexity have systemic implications.
Monolithic applications are generally deployed infrequently and as such significant startup and tear-down time/effort including some level of manual intervention is acceptable and often factored into service-level agreements (SLAs). Microservices are expected to auto-scale in response varying loads so they must be built as transient, lightweight components to allow for quick and automated spin-up and tear-down.
Non-blocking, event-driven, asynchronous operations
In Monolithic applications, the vast majority of operations are synchronous and blocking. Events are usually internal, and used to facilitate decoupling of distinct modules. Asynchronous operations are usually delegated to Job Queues. Since performance and availability is highly prioritized in MSA, non-blocking, asynchronous operations are preferred over blocking calls where clients wait "online" for the results of operations. Events are used to decouple and parallelize operations for scalability and resilience.
The transactional model of relational databases (ACID) dominates how data is processed and persisted in monolithic applications. Operations are modeled to be binary in nature – either a success or a failure – and the result is immediately relayed to the user. While the "happy path" in MSA can model atomic transactions, it's distributed nature brings to the surface issues of strong and weak consistency. In order to offer high resiliency, performance and availability microservices are built to tolerate varying latencies and partial failures, which may involve embracing both strong and weak consistency models.
Monolithic applications typically default to single-threaded concurrency models and when multi-threaded, concurrent processing is invoked it is carefully coordinated to prevent race-conditions and other undesirable outcomes. The standard architecture patterns for MSA always assume concurrent processing and as such start at a much higher level of complexity in terms of process interactions. Managing and coordinating concurrent processing in a central architectural concern at the system level. Microservices are built with the intent of running many concurrent instances as consumers and producers with the system.
The distributed nature of microservices and the focus on performance forces observability to the forefront of design. Viewing the internal state, logs and other operational details of each instance is key to understanding the system as a whole and so they expose as much information as possible in standardized, consumable formats to gather metrics, analytics and maintain audit logs of operations. This becomes even more critical when instances are transient i.e quickly spun-up to perform an operation and torn-down immediately.
Image Credits: https://www.nginx.com/blog/introduction-to-microservices/