In the world of modern application development, stateless architectures have become the norm. They're scalable, easy to reason about, and fit well with cloud-native paradigms. But what happens when you need to build features that are inherently stateful? What if you're developing a real-time chat application, a game server, or a system that needs to track complex workflows?
The Stateful Challenge
As a Spring Boot developer, I've often found myself in this situation. Spring Boot excels at building REST APIs, microservices, and traditional web applications. However, when it comes to maintaining state across requests or implementing real-time features, things get complicated.
Consider these common challenges:
Managing state across requests: In a stateless architecture, you typically push state to an external database or cache. This works, but adds latency and complexity.
Building real-time applications: Applications like chat systems require maintaining connections and state for each user.
Handling complex workflows: Some business processes involve multiple steps and state transitions that are awkward to model in a stateless way.
The traditional solutions often involve:
- Using Redis for pub/sub and state management
- Implementing WebSocket handlers with custom state tracking
- Creating complex state machines with external persistence
These approaches work, but they often feel like fighting against the framework rather than working with it.
Enter the Actor Model
The actor model is a programming paradigm that's particularly well-suited for building stateful, concurrent systems. In this model:
- Actors are lightweight entities that encapsulate state and behavior
- Actors communicate by sending messages to each other
- Each actor processes messages one at a time, eliminating concurrency issues
Frameworks like Akka (and its Apache-licensed fork, Pekko) have implemented the actor model successfully, but integrating them with Spring Boot has always been challenging. The programming models feel different, and making them work together requires boilerplate code and careful consideration.
Bridging the Gap: Spring Boot Starter Actor
That's why I created Spring Boot Starter Actor, a library that seamlessly integrates the actor model with Spring Boot using Pekko. This library allows you to:
- Auto-configure Pekko with Spring Boot
- Create and manage actors using familiar Spring patterns
- Leverage Spring's dependency injection within your actors
- Run in both local and clustered modes
- Build distributed, stateful applications with minimal boilerplate
How It Works
Let's look at a simple example of how you might use Spring Boot Starter Actor to build a stateful application.
First, add the dependency to your project:
implementation 'io.github.seonwkim:spring-boot-starter-actor:0.0.3'
Then, enable actor support in your application.yml
:
spring:
actor-enabled: true
Now, you can create an actor that maintains state:
@Component
public class HelloActor implements SpringActor {
public interface Command {}
public static class SayHello implements Command {
public final ActorRef<String> replyTo;
public SayHello(ActorRef<String> replyTo) {
this.replyTo = replyTo;
}
}
@Override
public Class<?> commandClass() {
return Command.class;
}
@Override
public Behavior<Command> create(String id) {
return Behaviors.setup(ctx -> new HelloActorBehavior(ctx, id).create());
}
private static class HelloActorBehavior {
private final ActorContext<Command> ctx;
private final String actorId;
HelloActorBehavior(ActorContext<Command> ctx, String actorId) {
this.ctx = ctx;
this.actorId = actorId;
}
public Behavior<Command> create() {
return Behaviors.receive(Command.class)
.onMessage(SayHello.class, this::onSayHello)
.build();
}
private Behavior<Command> onSayHello(SayHello msg) {
ctx.getLog().info("Received SayHello for id={}", actorId);
msg.replyTo.tell("Hello from actor " + actorId);
return Behaviors.same();
}
}
}
And use it in a service:
@Service
public class HelloService {
private final SpringActorRef<Command> helloActor;
public HelloService(SpringActorSystem springActorSystem) {
this.helloActor = springActorSystem.spawn(HelloActor.Command.class, "default")
.toCompletableFuture()
.join();
}
public Mono<String> hello() {
return Mono.fromCompletionStage(helloActor.ask(HelloActor.SayHello::new, Duration.ofSeconds(3)));
}
}
Real-World Example: Building a Chat Application
One of the most compelling use cases for the actor model is building real-time distributed chat applications. With Spring Boot Starter Actor, this becomes straightforward.
The library includes a complete chat example that demonstrates:
Chat Room Actors: Each chat room is a separate actor that maintains a list of connected users and broadcasts messages.
User Actors: Each connected user has an actor that receives events from chat rooms and forwards them to WebSocket sessions.
Spring Integration: Standard Spring services coordinate between WebSockets and the actor system.
Here's a simplified look at how a chat room actor might be implemented:
@Component
public class ChatRoomActor implements ShardedActor<ChatRoomActor.Command> {
// Command and event definitions...
@Override
public Behavior<Command> create(EntityContext<Command> ctx) {
return Behaviors.setup(context -> {
final String roomId = ctx.getEntityId();
return chatRoom(roomId, new HashMap<>());
});
}
private Behavior<Command> chatRoom(String roomId, Map<String, ActorRef<ChatEvent>> connectedUsers) {
return Behaviors.receive(Command.class)
.onMessage(JoinRoom.class, msg -> {
// Add the user to connected users
connectedUsers.put(msg.userId, msg.userRef);
// Notify all users that a new user has joined
UserJoined event = new UserJoined(msg.userId, roomId);
broadcastEvent(connectedUsers, event);
return chatRoom(roomId, connectedUsers);
})
.onMessage(LeaveRoom.class, msg -> {
// Remove the user from connected users
connectedUsers.remove(msg.userId);
// Notify all users that a user has left
UserLeft event = new UserLeft(msg.userId, roomId);
broadcastEvent(connectedUsers, event);
return chatRoom(roomId, connectedUsers);
})
.onMessage(SendMessage.class, msg -> {
// Broadcast the message to all connected users
MessageReceived event = new MessageReceived(msg.userId, msg.message, roomId);
broadcastEvent(connectedUsers, event);
return Behaviors.same();
})
.build();
}
private void broadcastEvent(Map<String, ActorRef<ChatEvent>> connectedUsers, ChatEvent event) {
connectedUsers.values().forEach(userRef -> userRef.tell(event));
}
}
This approach provides several benefits:
- Natural state management: The actor maintains the list of connected users and handles message broadcasting.
- Concurrency safety: The actor processes one message at a time, eliminating race conditions.
- Scalability: Using sharded actors, the chat rooms can be distributed across a cluster.
- Fault tolerance: If a node fails, the actors can be recreated on other nodes.
Benefits of Using Spring Boot Starter Actor
By integrating the actor model with Spring Boot, this library offers several advantages:
Simplified state management: Actors naturally encapsulate state, making it easier to reason about and maintain.
Reduced boilerplate: The library handles the integration between Spring and Pekko, allowing you to focus on your business logic.
Familiar programming model: You can continue using Spring's dependency injection and other features while leveraging the actor model.
Scalability: The library supports both local and clustered modes, allowing your application to scale as needed.
Resilience: Actors provide supervision hierarchies and failure handling mechanisms that make your application more resilient.
Conclusion
Building stateful applications with Spring Boot doesn't have to be difficult. Spring Boot Starter Actor bridges the gap between Spring's ease of use and the actor model's natural approach to state management.
Whether you're building a chat application, a game server, or any system that requires maintaining state, this library provides a clean, intuitive way to do so while staying within the Spring ecosystem.
Give it a try for your next stateful application, and experience how the actor model can simplify your code and make your system more robust.
Top comments (0)