Akka with Java 21: less is more
Akka 24.05 has been officially certified on Java 21, the latest Long-Term Support (LTS) version, and can now be used with your Akka applications.
At the center of Java 21 are enhancements to Records (JEP 440) and Pattern Matching (JEP 441) features. A Record provides a compact syntax for declaring special classes, which are transparent holders for shallowly immutable data. Pattern Matching allows the conditional extraction of components from objects based on their structure and type. This simplifies code by reducing the need for explicit casting and complex instance checks. Both were introduced in previous releases and have been improved and extended in subsequent releases, including in Java 21.
In this blog post, we will show how these and other new Java features allow you to build more concise, expressive, and reliable Akka applications with a cleaner codebase.
Sample snippet
This is how an Actor will look like when using records for commands, and a single command handler taking advantage of pattern matching:
public class MathGenius extends AbstractOnMessageBehavior<MathGenius.Command> {
// define the messages that this actor can handle
public sealed interface Command {
ActorRef<Result> replyTo();
}
public record Add(int a, int b, ActorRef<Result> replyTo) implements Command {}
public record Subtract(int a, int b, ActorRef<Result> replyTo) implements Command {}
public record Multiply(int a, int b, ActorRef<Result> replyTo) implements Command {}
public record Divide(int a, int b, ActorRef<Result> replyTo) implements Command {}
// define the response type
public record Result(float x) {}
public static Behavior<Command> create() {
return Behaviors.setup(MathGenius::new);
}
private MathGenius(ActorContext<Command> context) {
super(context);
}
@Override
public Behavior<Command> onMessage(Command message) {
float result = switch (message) {
case Multiply(int a, int b, var __) -> a * b;
case Add(int a, int b, var __) -> a + b;
case Divide(int a, int b, var __) -> (float) a / b;
case Subtract(int a, int b, var __) -> a - b;
};
message.replyTo().tell(new Result(result));
return this;
}
}
This MathGenius
actor handles mathematical operations asynchronously. Although this is a simple example of an Actor, there are a few things worth highlighting.
Records for message passing
In the sample above we define a sealed interface Command
as a general type for the messages it can handle, implementing its concrete types Add
, Subtract
, Multiply
, and Divide
as records, each carrying the operation's operands and a reference to where the result should be sent.
In Akka applications, messages passed between actors often carry data payloads. Using records for the message types reduces boilerplate code when compared with defining classes with data fields, as it automatically generates constructors, accessors, equals()
, hashCode()
, and toString()
methods.
Additionally, records are immutable by default**, which aligns well with the actor model's principles of message immutability, promoting better concurrency and easier reasoning about code. When building event sourced entities with EventSourcedBehavior
, the use of records to encode the State, Events, and Commands is a natural fit as it will make the codebase more concise and easy to reason about.
**Arguable since fields are final, but there can still be mutable objects being used (e.g. an ArrayList), which would break its immutability. See the article from reflectoring.io for details.
Pattern matching and record deconstruction
By implementing AbstractOnMessageBehavior
(rather than AbstractBehavior
) we have the option to directly handle all the message types using the Command
interface rather than declaring a message matcher and method for each type. Handling all the different message types is facilitated by pattern matching: each case of the switch statement matches a specific record type and extracts the operands (a and b). This approach simplifies conditional logic, making it more expressive and less error-prone compared to traditional instances of checks and casting.
With the introduction of pattern matching and records, error handling in Akka applications in Java can be more robust. Pattern matching allows developers to easily handle different cases or error scenarios within actor behaviors, leading to clearer and more explicit error handling logic. Moreover, the use of sealed interfaces and the enhancements in pattern matching puts the burden on the compiler, which will now warn you of lack of exhaustiveness of switch expressions and statements (i.e. there is no switch expression covering a specific branch of the code - a new type not being handled) and also when there is dominance of labels (i.e. a switch branch never being hit because the previous ones catch all cases).
Exploring further
If you’re curious to see a realistic application, look at our updated tutorial in the Akka Guide.
There, in the context of a shopping cart application, you can find:
- a ShoppingCart Event Sourced entity using records to define its State, Commands, and Events as concisely as possible;
- an Akka projection handler using pattern matching to process events from the above mentioned entity.
If you rather explore the code on your IDE, the complete code sample can be downloaded from here.
Final remarks
Passing data around Actors in the form of messages or handling different event types are the bread and butter of every Akka application. Whether processing events from an Event Sourced entity or defining behaviors for an Actor, the combination of Records and Pattern Matching comes in handy. Instead of lengthy if-else statements, pattern matching enables developers to match on the structure of messages directly, leading to more readable and maintainable code. In this regard, Java has come a long way in recent years.
With Akka 24.05, building your Akka applications with Java 21 is now certified. Besides Akka, the latest release of Akka Insights also supports Java 21. Considering Java 21 is an LTS version we believe this is a good time for anyone who develops Akka applications with Java to consider upgrading their codebases to enjoy these benefits.
There are a few other improvements to the Akka Java API on the horizon. For instance, when building an Event Sourced entity, specifying command and event handlers could be allowed without needing to use a builder. Stay tuned for that and other improvements in upcoming Akka releases!
Posts by this author