Spring Boot Integration
How to leverage spring-boot-starter-actor with Spring Boot's ecosystem.
Core Philosophy
Actors are just Spring components. They use dependency injection, configuration, and all standard Spring Boot features—no special setup required.
Spring-Native
The library is designed to feel like native Spring Boot. If you know Spring Boot, you already know how to use actors.
Quick Start
1. Enable Actor Support
@SpringBootApplication
@EnableActorSupport // That's it!
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2. Create an Actor
@Component // Regular Spring component
public class OrderActor implements SpringActor<Command> {
private final OrderRepository repository; // Injected automatically
private final OrderService service; // Any Spring bean works
// Constructor injection like any Spring component
public OrderActor(OrderRepository repository, OrderService service) {
this.repository = repository;
this.service = service;
}
@Override
public SpringActorBehavior<Command> create(SpringActorContext ctx) {
return SpringActorBehavior.builder(Command.class, ctx)
.withState(actorCtx -> new OrderBehavior(actorCtx, repository, service))
.onMessage(CreateOrder.class, OrderBehavior::handleCreate)
.build();
}
private static class OrderBehavior {
private final ActorContext<Command> ctx;
private final OrderRepository repository;
private final OrderService service;
public OrderBehavior(ActorContext<Command> ctx, OrderRepository repository, OrderService service) {
this.ctx = ctx;
this.repository = repository;
this.service = service;
}
private Behavior<Command> handleCreate(CreateOrder cmd) {
// Implementation here
return Behaviors.same();
}
}
}
3. Use Actors from Controllers
@RestController
public class OrderController {
private final SpringActorSystem actorSystem; // Auto-configured
public OrderController(SpringActorSystem actorSystem) {
this.actorSystem = actorSystem;
}
@PostMapping("/orders/{orderId}")
public CompletionStage<OrderResponse> createOrder(
@PathVariable String orderId,
@RequestBody CreateOrderRequest request) {
return actorSystem.getOrSpawn(OrderActor.class, orderId)
.thenCompose(ref -> ref.ask(new CreateOrder(request))
.withTimeout(Duration.ofSeconds(5))
.execute());
}
}
Spring Data Integration
JPA Repositories
Inject JPA repositories directly into actors:
@Component
public class OrderActor implements SpringActor<Command> {
private final OrderRepository orderRepository;
public OrderActor(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Override
public SpringActorBehavior<Command> create(SpringActorContext ctx) {
return SpringActorBehavior.builder(Command.class, ctx)
.withState(actorCtx -> {
// Load state on startup from database
Order order = orderRepository.findByOrderId(ctx.actorId()).orElse(null);
return new OrderBehavior(actorCtx, orderRepository, order);
})
.onMessage(UpdateOrder.class, OrderBehavior::handleUpdate)
.build();
}
private static class OrderBehavior {
private final ActorContext<Command> ctx;
private final OrderRepository orderRepository;
private Order currentOrder;
public OrderBehavior(ActorContext<Command> ctx, OrderRepository orderRepository, Order order) {
this.ctx = ctx;
this.orderRepository = orderRepository;
this.currentOrder = order;
}
private Behavior<Command> handleUpdate(UpdateOrder cmd) {
if (currentOrder == null) {
cmd.reply(new OrderResponse(false, null, "Order not found"));
return Behaviors.same();
}
currentOrder.setAmount(cmd.getNewAmount());
currentOrder = orderRepository.save(currentOrder); // Explicit save
ctx.getLog().info("Order updated: {}", currentOrder.getOrderId());
cmd.reply(new OrderResponse(true, currentOrder, "Order updated"));
return Behaviors.same();
}
}
}
Reactive Repositories
Use reactive repositories for non-blocking operations:
@Component
public class ReactiveOrderActor implements SpringActor<Command> {
private final ReactiveOrderRepository repository;
public ReactiveOrderActor(ReactiveOrderRepository repository) {
this.repository = repository;
}
@Override
public SpringActorBehavior<Command> create(SpringActorContext ctx) {
return SpringActorBehavior.builder(Command.class, ctx)
.withState(actorCtx -> new ReactiveBehavior(actorCtx, repository))
.onMessage(SaveOrder.class, ReactiveBehavior::handleSave)
.onMessage(SaveComplete.class, ReactiveBehavior::handleComplete)
.build();
}
// Internal messages for async results
private record SaveComplete(Order order) implements Command {}
private static class ReactiveBehavior {
private final ActorContext<Command> ctx;
private final ReactiveOrderRepository repository;
private Order currentOrder;
public ReactiveBehavior(ActorContext<Command> ctx, ReactiveOrderRepository repository) {
this.ctx = ctx;
this.repository = repository;
}
private Behavior<Command> handleSave(SaveOrder cmd) {
repository.save(currentOrder)
.subscribe(
saved -> ctx.getSelf().tell(new SaveComplete(saved)),
error -> ctx.getLog().error("Save failed", error)
);
return Behaviors.same();
}
private Behavior<Command> handleComplete(SaveComplete msg) {
this.currentOrder = msg.order();
ctx.getLog().info("Order saved successfully: {}", currentOrder.getId());
return Behaviors.same();
}
}
}
Handling Blocking Operations
Database operations (JPA, JDBC) are blocking and should not run on the default actor dispatcher. Use a dedicated blocking dispatcher to prevent thread starvation.
@RestController
public class OrderController {
private final SpringActorSystem actorSystem;
@PostMapping("/orders/{orderId}")
public CompletionStage<OrderResponse> createOrder(@PathVariable String orderId, ...) {
return actorSystem.actor(OrderActor.class)
.withId(orderId)
.withBlockingDispatcher() // Configure dispatcher here
.spawn()
.thenCompose(ref -> ref.ask(new CreateOrder(...))
.withTimeout(Duration.ofSeconds(5))
.execute());
}
}
Thread Starvation
Never run blocking database operations on the default dispatcher. Always use a blocking dispatcher or configure a custom thread pool.
See the Dispatchers guide for detailed configuration and best practices.
Spring Boot Features
Configuration Properties
Use @ConfigurationProperties for actor configuration:
@ConfigurationProperties(prefix = "app.orders")
public class OrderConfig {
private int maxRetries = 3;
private Duration timeout = Duration.ofSeconds(5);
private SnapshotConfig snapshot = new SnapshotConfig();
public static class SnapshotConfig {
private int operationInterval = 100;
private long timeIntervalMillis = 60000;
// getters/setters
}
// getters/setters
}
app:
orders:
max-retries: 5
timeout: 10s
snapshot:
operation-interval: 50
time-interval-millis: 30000
Inject into actors:
@Component
public class OrderActor implements SpringActor<Command> {
private final OrderRepository orderRepository;
private final OrderConfig config; // Injected automatically
public OrderActor(OrderRepository orderRepository, OrderConfig config) {
this.orderRepository = orderRepository;
this.config = config;
}
@Override
public SpringActorBehavior<Command> create(SpringActorContext ctx) {
return SpringActorBehavior.builder(Command.class, ctx)
.withState(actorCtx -> {
SnapshotStrategy strategy = new HybridSnapshotStrategy(
config.getSnapshot().getOperationInterval(),
config.getSnapshot().getTimeIntervalMillis()
);
return new OrderBehavior(actorCtx, orderRepository, strategy);
})
.onMessage(CreateOrder.class, OrderBehavior::handleCreate)
.build();
}
}
Transaction Management
Actors can use Spring's declarative transactions:
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final OrderItemRepository itemRepository;
@Transactional // Standard Spring transaction
public Order createOrderWithItems(String orderId, List<OrderItem> items) {
Order order = new Order(orderId);
order = orderRepository.save(order);
for (OrderItem item : items) {
item.setOrder(order);
itemRepository.save(item);
}
return order;
}
}
@Component
public class OrderActor implements SpringActor<Command> {
private final OrderService orderService; // Inject service with @Transactional
@Override
public SpringActorBehavior<Command> create(SpringActorContext ctx) {
return SpringActorBehavior.builder(Command.class, ctx)
.withState(actorCtx -> new OrderBehavior(actorCtx, orderService))
.onMessage(CreateOrder.class, OrderBehavior::handleCreate)
.build();
}
private static class OrderBehavior {
private final ActorContext<Command> ctx;
private final OrderService orderService;
public OrderBehavior(ActorContext<Command> ctx, OrderService orderService) {
this.ctx = ctx;
this.orderService = orderService;
}
private Behavior<Command> handleCreate(CreateOrder cmd) {
List<OrderItem> items = cmd.getItems();
Order order = orderService.createOrderWithItems(ctx.actorId(), items);
// Transaction is managed by Spring
ctx.getLog().info("Order created with items: {}", order.getOrderId());
cmd.reply(new OrderResponse(true, order, "Order created"));
return Behaviors.same();
}
}
}
Complete Example
See example/persistence for a complete working example demonstrating:
- JPA integration with actors
- Transaction management
- Blocking dispatcher configuration
- State loading and persistence
Example Code
The persistence example shows a complete implementation of actors with Spring Data JPA integration.
Summary
Spring Boot integration is seamless:
- Actors are Spring components - Use
@Component, inject dependencies normally - SpringActorSystem is auto-configured - Just inject and use
- Use Spring Data directly - No adapters or wrappers needed
- Standard Spring Boot features work - Configuration, profiles, actuator, testing
- CompletionStage integrates with Spring MVC/WebFlux - Async responses just work
No Special Setup
No special setup. No custom infrastructure. Just Spring Boot + actors.
Next Steps
- Dispatchers - Configure thread execution for blocking operations
- Sharded Actors - Distribute actors across a cluster
- Logging with MDC - Add observability to your actors