Reactive Microservices: Driving Application Modernization Efforts
This article is featured in the new DZone Guide to Modern Java, Volume II. Get your free copy for more insightful articles, industry statistics, and more.
Even today, after almost two solid years, microservices are still one of the emerging topics in enterprises. With the challenges coming with a rising number of consumers and changing use cases for customers, microservices provide a more stable and less cost-intensive way to build applications. Though this isn’t the first time evolving technologies have shocked the well-oiled machine of software to its core, microservices adoption truly forces traditional enterprises tore-think what they’ve been doing for almost two decades. We’ve seen design paradigms change over time and project management methodologies evolve. But this time, it looks like the influence is far bigger than anything we’ve seen before. And the interesting part is that microservices aren’t new from the core.
A core skill of software architects is understanding modularization and components in software and designing appropriate dependencies between them. We've already learned how to couple services and build them around organizational capabilities, and it’s in looking beyond those binary dependencies, that exciting part of microservices-based architectures comes into play — how independent microservices are distributed and connected back together.Building an individual service is easy with all technologies. Building a system out of many is the real challenge because it introduces us to the problem space of distributed systems. This is a major difference from classical, centralized infrastructures. As a result, there are very few concepts from the old world which still fit into a modern architecture.
Microservices in a Reactive World
Up to now, the usual way to describe distributed systems has been to use a mix of technical and business buzzwords:asynchronous, non-blocking, real-time, highly-available,loosely coupled, scalable, fault-tolerant, concurrent,message-driven, push instead of pull, distributed, low latency, high throughput, etc. The Reactive Manifesto brought all these characteristics together, and it defines them through four high-level traits: Responsive, Resilient, Elastic, and Message-driven.Even if it looks like this describes an entirely new architectural pattern, the core principles have long been known in industries that require real-time IT systems, such as financial trading. If you think about a system composed out of individual services, you will quickly realize how closely the Reactive world is related to microservices. Let's explore the four main characteristics of a Reactive microservices system closer.
Responsive
A responsive application satisfies the consumer's expectations in terms of availability and real-time responses. Responsiveness is measured in latency, which is the time between request and response. Especially with many small requests by mobile or internet-connected devices, a microservices-based architecture can achieve this using the right design.
Resilient
A very high-quality service performs its function without any downtime at all. But failures do happen, and handling and recovering from the failure of an individual service in a gentle way without affecting the complete system is what the term resiliency describes. While monolithic applications need to be recovered completely and handling failures mostly comes down to proprietary infrastructure components, individual microservices can easily provide these features, for example by supervision.
Elastic
A successful service will need to be scalable both up and down in response to changes in the rate at which the service is used. While monoliths usually are scaled on a per server basis, the flexibility and elasticity that comes with microservices has a much greater potential to respond to changing workloads.
Message Driven
And basically, the only way to fulfill all the above requirements is to have loosely coupled services with explicit protocols communicating over messages. Components can remain inactive until a message arrives,freeing up resources while doing nothing. In order to realize this, non-blocking and asynchronous APIs must be provided that explicitly expose the system’s underlying message structure. While traditional frameworks (e.g.Spring and Java EE) have very little to offer here, modern approaches like Akka or Lagom are better equipped to help implement these requirements.
Architectural Concepts of Reactive Microservices Architectures
Becoming a good architect of microservice systems requires time and experience. However, there are attempts to guide your decision-making and experiences with frameworks providing opinionated APIs, features, and defaults. The following examples are built with the Lagomframework. It provides tools and APIs that guide developers to build systems out of microservices. The key to success is good architecture, rooted in a solid understanding of microservices concepts and best practices, and Lagomencapsulates these for you to use in the form of APIs.
Isolation
It is not enough to build individual services. These services also need to isolate their failures. Containing and managing them without cascading throughout all the services participating in a complete workflow is a pattern referred to as bulkheading. This includes the ability to heal from failure, which is called resilience. And this highly depends on compartmentalization and containment of failure. Isolation also makes it easier to scale services on demand and also allows for monitoring, debugging, and testing them independently.
Autonomy
Acting autonomously as a microservice also means that those services can only promise their behavior by publishing their protocols and APIs. And this gives a great amount of flexibility around service orchestration,workflow management, and collaborative behavior. Through communication over well-defined protocols, autonomous services also add to resilience and elasticity. Lagom services are described by an interface, known as a service descriptor. This interface not only defines how the service is invoked and implemented, it also defines the metadata that describes how the interface is mapped down onto an underlying transport protocol.
public interface HelloService extends Service {
ServiceCall sayHello();
default Descriptor descriptor() {
return named("hello").withCalls(
call(this::sayHello)
);
}
}
Services are implemented by providing an implementation of the service descriptor interface, implementing each call specified by that descriptor.
public class HelloServiceImpl implements HelloService {
public ServiceCall sayHello() {
return name -> completedFuture("Hello " + name);
}
}
Single Responsibility Principle
One of the most pressing questions for microservices has always been about size. What can be considered “micro”? How big in terms of lines of code or JAR file size is the optimal microservice? But these questions really aren’t at the heart of the problem. Instead, “micro” should refer to scope of responsibility. One of the best guiding principles here is the Unix philosophy: let it do one thing, and do it well. When you look at refactoring existing systems, it will help to find a verb or noun as an initial description of a microservice. If a service only has one single reason to exist, providing a single composable piece of functionality, then business domains and responsibilities are not tangled. Each service can be made more generally useful, and the system as a whole is easier to scale, make resilient,understand, extend, and maintain.
Exclusive State
Microservices are often called stateful entities: they encapsulate state and behavior, in a similar fashion to an Object, and isolation most certainly applies to state and requires that you treat state and behavior as a single unit. Just pushing the state down to a shared database and calling it a stateless architecture isn’t the solution.Each microservice needs to take sole responsibility for its own state and the persistence thereof. This opens up possibilities for the most adequate persistence technology, ranging from RDBMS to Event-Log driven systems like Kafka. Abstracted techniques such as Event Sourcing and Command Query Responsibility Segregation (CQRS) serve as the persistence technology for reactive microservices.
In Lagom, a PersistentEntity has a stable entity identifier, with which it can be accessed from the service implementation or other places. The state of an entity is persistent using Event Sourcing. All state changes are represented as events and those immutable facts are appended to an event log. To recreate the current state of an entity when it is started, all those events get replayed.You interact with a PersistentEntity by sending command messages to it. An entity stub can look like this:
public class Post
extends PersistentEntity<BlogCommand, BlogEvent,
BlogState> {
@Override
public Behavior initialBehavior(Optional<BlogState>
snapshotState) {
BehaviorBuilder b = newBehaviorBuilder(
snapshotState.orElse(BlogState.EMPTY));
// Command and event handler go here
return b.build();
}
}
The three type parameters of the extended PersistentEntity class define: the Command, the superclass/interface of the commands; the Event, the superclass/interface of the events; and the State, the class of the state.
Asynchronous Message-Passing
Communication between microservices needs to be based on asynchronous message-passing.
Besides being the only effective help in isolating the individual services, the non-blocking execution and Input/Output (IO) is most often more effective on resources. Unfortunately, one of the most common implementations of the REST architecture, REST over HTTP, is widely considered the default microservices communication protocol. If you’re looking into this, you need to be aware that the implementations most often are synchronous and as such not a suitable fit for microservices as the default protocol. All Lagom APIs use the asynchronous IO capabilities of Akka Stream for asynchronous streaming and the JDK8 CompletionStage API for asynchronous computation. Furthermore, Lagom makes asynchronous communication the default: when communicating between services, streaming is provided as a first-class concept. Developers are encouraged and enabled to use asynchronous messaging via streaming, rather than synchronous request-response communication. Asynchronous messaging is fundamental to a system's resilience and scalability. A streamed message is a message of type Source. Source is an Akka streams API that allows asynchronous streaming and handling of messages. Here’s an example streamed service call:
ServiceCall<String, Source<String, ?>> tick(int
interval);
default Descriptor descriptor() {
return named("clock").withCalls(
pathCall("/tick/:interval", this::tick)
);
}
Mobility
Another requirement for microservices is the ability to run independently of the physical location. And asynchronous message-passing provides the needed decoupling, which is also called location transparency: this gives the ability to, at runtime, dynamically scale the microservice—either on multiple cores or on multiplenodes—without changing the code. This kind of service distribution is needed to take full advantage of cloud computing infrastructures with microservices. A successful microservices services architecture needs to be designed with the core traits of Reactive Microservices in mind. Isolation, Single Responsibility, Autonomy, Exclusive State, Asynchronous Message-Passing, and Mobility are required to build out a reactive microservice architecture. The most interesting, rewarding, and challenging partstake place when microservices collaborate and build acomplete system. This is the chance to learn from pastfailures and successes in distributed systems andmicroservices-based architectures.
You can learn even more about reactive microservice architectures by reading Jonas Bonér’s free O’Reilly book, and you can get a deep-dive into Lagom with my own mini-book about implementing reactive microservices.