If you've been building Spring Boot applications for a while, you've probably run into scenarios where traditional approaches to concurrency start to feel clunky. Maybe you're building a real-time chat application, an IoT platform processing thousands of device messages, or an event-driven system that needs to handle high throughput without losing its mind.
The actor model is a proven solution for these kinds of problems. But here's the thing: the Spring ecosystem doesn't have native actor support. And if your company has standardized on Spring, introducing a completely different framework just to get actors isn't exactly a trivial decision.
That's where spring-boot-starter-actor comes in.
The Actor Model Gap in Spring
Spring is great at what it does. Dependency injection, configuration management, data access—it's all there. But when it comes to building highly concurrent systems with isolated state and message passing, you're mostly on your own.
Meanwhile, in the JVM world, Pekko (an open-source fork of Akka) has been the go-to framework for actor-based systems. It's battle-tested, scales horizontally, and handles fault tolerance elegantly through supervision hierarchies. The problem? Integrating Pekko into a Spring application isn't straightforward. You end up writing a lot of glue code, managing two different dependency injection systems, and generally making things more complicated than they need to be.
What is spring-boot-starter-actor?
spring-boot-starter-actor is a Spring Boot starter that bridges this gap. It brings Pekko's actor model into Spring Boot applications with minimal friction. You get:
- Auto-configuration: Just add the dependency and you're ready to go
- Full Spring DI support: Your actors are Spring components with constructor injection
- Local and cluster modes: Start simple, scale when you need to
- Built-in monitoring: Prometheus metrics and Grafana dashboards included
Getting Started
Add the dependency to your project:
dependencyManagement {
imports {
mavenBom("com.fasterxml.jackson:jackson-bom:2.17.3")
}
}
// Spring Boot 3.2.x
implementation 'io.github.seonwkim:spring-boot-starter-actor_3:0.6.3'
Enable actor support in your application:
@SpringBootApplication
@EnableActorSupport
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
Creating Your First Actor
Actors are just Spring components that implement SpringActor:
@Component
public class GreeterActor implements SpringActor<GreeterActor.Command> {
public interface Command {}
public static class Greet extends AskCommand<String> implements Command {
public final String name;
public Greet(String name) {
this.name = name;
}
}
@Override
public SpringActorBehavior<Command> create(SpringActorContext actorContext) {
return SpringActorBehavior.builder(Command.class, actorContext)
.onMessage(Greet.class, (ctx, msg) -> {
msg.reply("Hello, " + msg.name + "!");
return Behaviors.same();
})
.build();
}
}
Each actor processes messages one at a time, so you never have to worry about concurrent access to its internal state. No locks, no synchronized blocks, no ConcurrentHashMap—just straightforward sequential code.
Using Actors in Your Services
Inject SpringActorSystem anywhere you need to work with actors:
@Service
public class GreeterService {
private final SpringActorSystem actorSystem;
public GreeterService(SpringActorSystem actorSystem) {
this.actorSystem = actorSystem;
}
public CompletionStage<String> greet(String name) {
return actorSystem.getOrSpawn(GreeterActor.class, "greeter")
.thenCompose(actor -> actor
.ask(new GreeterActor.Greet(name))
.withTimeout(Duration.ofSeconds(5))
.execute()
);
}
}
The getOrSpawn method either returns an existing actor or creates a new one. Actors have unique IDs within their type, so you can have thousands of greeter actors, each identified by a different ID.
Spring DI Just Works
Since actors are Spring components, you can inject whatever you need:
@Component
public class OrderActor implements SpringActor<OrderActor.Command> {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
public OrderActor(OrderRepository orderRepository,
PaymentService paymentService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
}
// ... actor implementation
}
Your repositories, services, configurations—everything Spring knows about is available to your actors through regular constructor injection.
Scaling with Sharded Actors
When you're ready to distribute your actors across multiple nodes, switch to cluster mode and use sharded actors:
@Component
public class UserSessionActor implements SpringShardedActor<UserSessionActor.Command> {
public static final EntityTypeKey<Command> TYPE_KEY =
EntityTypeKey.create(Command.class, "UserSession");
public interface Command extends JsonSerializable {}
public record UpdateActivity(String activity) implements Command {}
@Override
public EntityTypeKey<Command> typeKey() {
return TYPE_KEY;
}
@Override
public SpringShardedActorBehavior<Command> create(SpringShardedActorContext<Command> ctx) {
return SpringShardedActorBehavior.builder(Command.class, ctx)
.withState(entityCtx -> new UserSessionState(ctx.getEntityId()))
.onMessage(UpdateActivity.class, UserSessionState::onUpdateActivity)
.build();
}
}
Sharded actors are automatically distributed across your cluster nodes. When a message arrives for user "user-123", the cluster routes it to the right node. If that node goes down, the actor moves to another node. You don't have to manage any of this—it just works.
Real-World Use Cases
This isn't just theoretical. The library works well for:
- Real-time applications: Chat systems, live notifications, collaborative editing
- IoT platforms: Processing streams of device telemetry with per-device state
- Event processing: Building event-sourced systems with aggregate actors
- Rate limiting: Per-user or per-API-key rate limiters that maintain state
- Saga orchestration: Long-running workflows with state and retries
What About Fault Tolerance?
Actors can fail. When they do, supervision strategies determine what happens:
actorSystem.actor(WorkerActor.class)
.withId("worker-1")
.withSupervisionStrategy(SupervisorStrategy.restart().withLimit(3, Duration.ofMinutes(1)))
.spawn();
If an actor throws an exception, it can be automatically restarted, stopped, or resumed. Parent actors can supervise child actors, creating hierarchies that isolate failures and recover gracefully.
Try It Out
The library is available on Maven Central, and there's a full example of a distributed chat application you can run locally:
# Start a 3-node cluster
$ sh cluster-start.sh chat io.github.seonwkim.example.SpringPekkoApplication 8080 2551 3
# Run the frontend
$ cd example/chat/frontend
$ npm run dev
You can send messages between users, and they'll be routed correctly even though the actors are distributed across three different JVM processes.
Resources
- Documentation: https://seonwkim.github.io/spring-boot-starter-actor/
- GitHub: https://github.com/seonwkim/spring-boot-starter-actor
- Discord: Join the community
Final Thoughts
Building concurrent systems doesn't have to be painful. The actor model provides a proven way to handle concurrency, distribution, and fault tolerance. And now, with spring-boot-starter-actor, you can use it in your Spring Boot applications without leaving the ecosystem you already know.
Give it a try, and let me know what you build with it.
Top comments (1)
Thanks for this deep dive! The clear examples of wiring Pekko actors into Spring DI, plus the cluster/sharded actor section and built‑in Prometheus/Grafana support, really make the actor model feel practical for real Spring Boot systems.