DEV Community

Cover image for Bringing the Actor Model to Spring Boot with Pekko
Kim Seon Woo
Kim Seon Woo

Posted on

Bringing the Actor Model to Spring Boot with Pekko

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'
Enter fullscreen mode Exit fullscreen mode

Enable actor support in your application:

@SpringBootApplication
@EnableActorSupport
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
            );
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You can send messages between users, and they'll be routed correctly even though the actors are distributed across three different JVM processes.

Resources

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)

Collapse
 
youngfra profile image
Fraser Young

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.