As 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 is still a little blurry and the microservices hype is only slowly settling while the software industry explores the various architectures and implementation styles. And there are many ways to implement a microservices based architecture on or around the Java Virtual Machine (JVM).
Most promising seem to be the ones 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 supports 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 the Akka http. Although Akka is written in Scala, there is a Java API, too. 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 Akka documentation contains an extensive walk through of a simplistic IoT management application. It allows users to query sensor data. It does not expose any external API to keep things simpler and only focusses on the design of the application and uses an actor based API for devices to report their data back to the management part. This is a high-level architecture diagram of it.
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 that contained nested boxes into a tree.
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.
You find the details about how to implement the IoTSupervisor and DeviceManager classes in the official Akka tutorial.
How Actors communicate
Looking at the Device actor whose simple task is to collect temperature measurements and report them back on request. When working with objects you usually design APIs as interfaces, which are basically a collection of abstract methods to be filled out by the actual implementation. In the world of actors, the counterpart of interfaces are protocols. The protocol in an actor based application is the message for the devices (Code 1)
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; } }
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 “resend” 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 DeviceActor:
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(); } }
The current temperature is initially set to Optional.empty(), and simply reported back when queried. A simple test for the device is shown in code 3.
@Test public void testReplyWithEmptyReadingIfNoTemperatureIsKnown() { TestKit probe = new TestKit(system); ActorRef deviceActor = system.actorOf(Device.props("group", "device")); deviceActor.tell(new Device.ReadTemperature(42L), probe.getRef()); Device.RespondTemperature response = probe.expectMsgClass(Device.RespondTemperature.class); assertEquals(42L, response.requestId); assertEquals(Optional.empty(), response.value); }
(Code 3: Testing the device actor)
The complete example if the IoT System is contained in the Akka documentation.
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. As Martin Fowler states in his article about “MicroservicePremium”. 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. Systems of microservices.
Author: Markus Eisele
Markus Eisele leads developer tools marketing at Red Hat. He has been working with Java EE servers from different vendors for more than 14 years, and gives presentations on his favourite topics at leading international Java conferences. He is a Java Champion, former Java EE Expert Group member, and founder of JavaLand. He is excited to educate developers about how microservices architectures can integrate and complement existing platforms.
He is also the author of “Modern Java EE Design Patterns” and “Developing Reactive Microservices” by O’Reilly. You can follow more frequent updates on Twitter @myfear or Mastodon @myfear@mastodon.online