Microservices on the JVM With Actors
When it comes to bringing microservices to the JVM, it's hard to go wrong with Akka and its actor model. Let's see how they contribute to microservices adoption.
Join the DZone community and get the full member experience.
Join For FreeAs mobile and data-driven applications increasingly dominate, users are demanding real-time access to everything everywhere. System resilience and responsiveness are no longer “nice to have;” they’re essential business requirements. Businesses increasingly need to trade up from static, centralized architectures in favor of flexible, distributed, and elastic systems.
But where to start and which architecture approach to use is still a little blurry, and the microservices hype is only slowly settling while the software industry explores various architectures and implementation styles.
For a decade or more, enterprise development teams have built their Java EE projects inside large, monolithic application server containers without much regard to the individual lifecycle of their module or component. Hooking into startup and shutdown events was simple, as accessing other components was just an injected instance away. It was comparably easy to map objects into single relational databases or connect to other systems via messaging. One of the greatest advantages of this architecture was transactionality, which was synchronous, easy to implement, and
simple to visualize and monitor.
By keeping strong modularity and component separation a first-class priority, it was manageable to implement the largest systems that still power our world. Working with compartmentalization and introducing modules belongs to the core skills of architects. Our industry has learned how to couple services and build them around organizational capabilities.
The new part in microservices-based architectures is the way truly independent services are distributed and connected back together. Building an individual service is easy. Building a system out of many is the real challenge, because it introduces us to the problem space of distributed systems. This is the major difference from classical, centralized infrastructures.
There Isn't Just One Way of Doing Microservices
There are many ways to implement a microservices-based architecture on or around the Java Virtual Machine (JVM). The pyramid in Figure 1 was introduced in my first book. It categorizes some technologies into layers, which can help identify the level of isolation that is needed for a microservices-based system.
Starting at the virtualization infrastructure with virtual machines and containers, as they are means of isolating applications from hardware, we go all the way up the stack to something that I summarize under the name “application services.” This category contains specific microservices frameworks aimed at providing microservices support across the complete software development lifecycle.
Figure 1: Pyramid of modern enterprise Java development (Source: Modern
Java EE Design Patterns, Eisele)
The three frameworks in the application services and infrastructure categories are all based on the principles of the Reactive Manifesto. It defines traits that lead to large systems that are composed of smaller ones, which are more flexible, loosely-coupled, and scalable. As they are essentially message-driven and distributed, these frameworks fit the requirements of today’s microservices architectures.
While Lagom offers an opinionated approach on close guardrails that only support microservices architectures, Play and Akka allow you to take advantage of the reactive traits to build a microservices-style system but doesn’t limit you to this approach.
Microservices With Akka
Akka is a toolkit and runtime for building highly concurrent, distributed, and resilient message-driven applications on the JVM. Akka “actors” are one of the tools in the Akka toolkit that allow you to write concurrent code without having to think about low-level threads and locks. Other tools include Akka Streams and Akka HTTP. Although Akka is written in Scala, there is a Java API, too.
Actors were invented decades ago by Carl Hewitt. But, relatively recently, their applicability to the challenges of modern computing systems has been recognized and proven to be effective. The actor model provides an abstraction that allows you to think about your code in terms of communication, not unlike people in a large organization.
Systems based on the actor model using Akka can be designed with incredible resilience. Using supervisor hierarchies means that the parental chain of components is responsible for detecting and correcting failures, leaving clients to be concerned only about what service they require.
Unlike code written in Java that throws exceptions, clients of actor-based services never concern themselves with dealing with failures from the actor from which they are requesting a service. Instead, clients only must understand the request-response contract that they have with a given service, and possibly retry requests if no response is given in some time frame. When people talk about microservices, they focus on the “micro” part, saying that a service should be small.
I want to emphasize that the important thing to consider when splitting a system into services is to find the right boundaries between services, aligning them with bounded contexts, business capabilities, and isolation requirements. As a result, a microservices-based system can achieve its scalability and resilience requirements, making it easy to deploy and manage. The best way to understand something is to look at an example. The Akka documentation contains an extensive walkthrough of a simplistic IoT management application that allows users to query sensor data. It does not expose any external API to keep things simpler, only focuses on the design of the application, and uses an actor-based API for devices to report their data back to the management part. You can find a high-level architecture diagram in Figure 2.
Figure 2: IoT sample application architecture (Source: Akka documentation)
Actors are organized into a strict tree, where the lifecycle of every child is tied to the parent, and where parents are responsible for deciding the fate of failed children. All you need to do is to rewrite your architecture diagram so that it contains nested boxes into a tree, as shown in Figure 3.
In simple terms, every component manages the lifecycle of the subcomponents. No subcomponent can outlive the parent component. This is exactly how the actor hierarchy works. Furthermore, it is desirable that a component handles the failure of its subcomponents. A “contained-in” relationship of components is mapped to the “children-of” relationship of actors.
If you look at microservice architectures, you would have expected that the top-level components are also the top-level actors. That is indeed possible, but not recommended. As we don’t have to wire the individual services back together via external protocols and the Akka framework also manages the actor lifecycle, we can create a single top-level actor in the actor system and model the main services as children of this actor. The actor architecture is built on the same traits that a microservice architecture should rely on, which are isolation, autonomy, single responsibility, exclusive state, asynchronous communication, explicit communication protocols, and distribution and location transparency.
Figure 3: An Actor representation of the IoT architecture.
You find the details about how to implement the IoTSupervisor and DeviceManager classes in the official Akka tutorial. Until now, I only looked at the complete system at large. But there is also the individual actor that represents a device. His simple task will be to collect temperature measurements and report the last measured data back on request. When working with objects, you usually design APIs as interfaces, which are basically collections of abstract methods to be filled out by the actual implementation. In the world of actors, the counterparts of interfaces are protocols. The protocol in an actor-based application is the message for the devices.
function counter(state: AppState = 0, action: AppAction): public
static final class ReadTemperature {
long requestId;
public ReadTemperature(long requestId) {
this.requestId = requestId;
}
}
public static final class RespondTemperature {
long requestId;
Optional < Double > value;
public RespondTemperature(long requestId, Optional < Double >
value) {
this.requestId = requestId;
this.value = value;
}
}
Code 1: message protocol for the device actor
I am skipping a lot of background on message ordering and delivery guarantees. Designing a system with the assumption that messages can be lost in the network is the safest way to build a microservices-based architecture. This can be done, for example, by implementing a “re-send” functionality if a message gets lost. And this is the reason why the message also contains a requestId. It will now be the responsibility of the querying actor to match requests to actors. A first rough sketch of the Device Actor is below.
class Device extends AbstractActor {
//…
Optional < Double > lastTemperatureReading = Optional.empty();
@Override
public void preStart() {
log.info(“Device actor {} - {}
started”, groupId, deviceId);
}
@Override
public void postStop() {
log.info(“Device actor {} - {}
stopped”, groupId, deviceId);
}
@Override
// react to received messages of ReadTemperature
public Receive createReceive() {
return receiveBuilder()
.match(ReadTemperature.class, r - > {
getSender().tell(new RespondTemperature(r.requestId, lastTemperatureReading), getSelf());
})
.build();
}
}
Code 2: The device actor
The current temperature is initially set to Optional.empty(), and simply reported back when queried. A simple test for the device is shown below.
@Test
public void testReplyWithEmptyReadingIfNoTemperatureIsKnown() {
TestKit probe = new TestKit(system);
ActorRef deviceActor = system.actorOf(Device.props(“group”, “device”));
deviceActor.tell(new Device.ReadTemperature(42 L), probe.getRef());
Device.RespondTemperature response = probe.
expectMsgClass(Device.RespondTemperature.class);
assertEquals(42 L, response.requestId);
assertEquals(Optional.empty(), response.value);
}
Code 3: Testing the device actor
The complete example of the IoT System is contained in the Akka documentation.
Where to Get Started
Most of today’s enterprise software was built years ago and still undergoes regular maintenance to adopt the latest regulations or new business requirements. Unless there is a completely new business case or significant internal restructuring, the need to reconstruct a piece of software from scratch is rarely given.
If this is the case, it is commonly referred to as “greenfield” development, and you are free to select the base framework of your choice. In a “brownfield” scenario, you only want to apply the new architecture to a certain area of an existing application. Both approaches offer risks and challenges and there are advocates for both. The common ground for both scenarios is your knowledge of the business domain.
Especially in long-running and existing enterprise projects, this might be the critical path. They tend to be sparse on documentation, and it is even more important to have access to developers who are working in this domain and have firsthand knowledge.
The first step is an initial assessment to identify which parts of an existing application can take advantage of a microservices architecture. There are various ways to do this initial assessment. I suggest thinking about service characteristics. You want to identify either core or process services first.
While core services are components modeled after nouns or entities, the process services already contain complex business or flow logic.
Selective Improvements
The most risk-free migration approach is to only add selective improvements. By scraping out the identified parts into one or more services and adding the necessary glue to the original application, you’re able to scale out specific areas of your application in multiple steps.
The Strangler Pattern
First coined by Martin Fowler as the Strangler Application, the extraction candidates are move into a separate system which adheres to a microservices architecture, and the existing parts of the applications remain untouched. A load balancer or proxy decides which requests need to reach the original application and which go to the new parts. There are some synchronization issues between the two stacks. Most importantly, the existing application can’t be allowed to change the microservices’ databases.
Big Bang: Refactor an Existing System
In very rare cases, complete refactoring of the original application might be the right way to go. It’s rare because enterprise applications will need ongoing maintenance during the complete refactoring.
What’s more, there won’t be enough time to make a complete stop for a couple of weeks — or even months, depending on the size of the application — to rebuild it on a new stack. This is the least recommended approach because it carries a comparably high risk of failure.
When Not to Use Microservices
Microservices are the right choice if you have a system that is too complex to be handled as a monolith. And this is exactly what makes this architectural style a valid choice for enterprise applications.
As Martin Fowler states in his article about “Microservice Premium,” the main point is to not even consider using a microservices architecture unless you have a system that’s too large and complex to be built as a simple monolith. But it is also true that today, multicore processors, cloud computing, and mobile devices are the norm, which means that all-new systems are distributed systems right from the start.
And this also results in a completely different and more challenging world to operate in. The logical step now is to switch thinking from collaboration between objects in one system to a collaboration of individually scaling systems of microservices.
Summary
The actor model provides a higher level of abstraction for writing
concurrent and distributed systems, which shields the developer
from explicit locking and thread management. It provides the core
functionality of reactive systems, defined in the Reactive Manifesto
as responsive, resilient, elastic, and message-driven. Akka is an
actor-based framework that is easy to implement with full Java 8
Lambda support. Actors enable developers to design and implement
systems in ways that help focus more on the core functionality
and less on the plumbing. Actor-based systems are the perfect
foundation for quickly evolving microservices architectures.
Opinions expressed by DZone contributors are their own.
Comments