Skip to main content

Actor IDs

Cajun provides a flexible and powerful system for managing actor identities. This guide covers all the ways you can control how actors are identified in your system.

Table of Contents

Overview

Every actor in Cajun has a unique identifier (ID) that is used for:

  • Message routing: Sending messages to specific actors
  • Logging and debugging: Identifying actors in logs
  • Persistence: Recovering actor state after restarts
  • Hierarchical organization: Creating parent-child relationships

Cajun provides four ways to control actor IDs, with a clear priority system:

  1. Explicit IDs (Highest Priority) - Manually specify the exact ID
  2. ID Templates - Generate IDs using placeholders
  3. ID Strategies - Use predefined ID generation strategies
  4. System Default (Lowest Priority) - Falls back to UUID

ID Priority System

When you configure multiple ID settings, Cajun uses this priority order:

// Priority 1: Explicit ID wins
Pid actor = system.actorOf(Handler.class)
.withId("my-actor") // ← This is used
.withIdTemplate("user-{seq}") // ← Ignored
.withIdStrategy(IdStrategy.UUID) // ← Ignored
.spawn();
// Result: "my-actor"

// Priority 2: Template wins over strategy
Pid actor = system.actorOf(Handler.class)
.withIdTemplate("user-{seq}") // ← This is used
.withIdStrategy(IdStrategy.UUID) // ← Ignored
.spawn();
// Result: "user-1", "user-2", etc.

// Priority 3: Strategy is used
Pid actor = system.actorOf(Handler.class)
.withIdStrategy(IdStrategy.CLASS_BASED_SEQUENTIAL) // ← This is used
.spawn();
// Result: "myhandler:1", "myhandler:2", etc.

// Priority 4: System default (UUID)
Pid actor = system.actorOf(Handler.class)
.spawn();
// Result: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

Important: Each withId(), withIdTemplate(), and withIdStrategy() call replaces any previous ID configuration. Only the last one in the chain is effective.

Explicit IDs

Manually specify the exact ID for an actor. Best for:

  • Singleton actors with well-known names
  • Actors that need to be looked up by name
  • Testing and debugging

Usage

// Simple explicit ID
Pid actor = system.actorOf(MyHandler.class)
.withId("user-service")
.spawn();

// Explicit IDs can contain any characters
Pid actor = system.actorOf(MyHandler.class)
.withId("user:123:profile")
.spawn();

// Unicode characters are supported
Pid actor = system.actorOf(MyHandler.class)
.withId("actor-测试-🎭")
.spawn();

Pros and Cons

Pros:

  • ✅ Predictable and easy to debug
  • ✅ Can be looked up by name
  • ✅ Great for singleton services

Cons:

  • ⚠️ You must ensure uniqueness manually
  • ⚠️ Not suitable for dynamic actor creation
  • ⚠️ Duplicate IDs will cause errors

ID Templates

Generate IDs dynamically using placeholders. Best for:

  • Creating multiple actors with consistent naming
  • Including dynamic information in IDs
  • Maintaining readable IDs with auto-incrementing counters

Available Placeholders

PlaceholderDescriptionExample Output
{seq}Auto-incrementing sequence number1, 2, 3
{template-seq}Sequence per template pattern1, 2, 3
{uuid}Full UUIDa1b2c3d4-e5f6-7890-abcd-ef1234567890
{short-uuid}First 8 characters of UUIDa1b2c3d4
{timestamp}Current timestamp (milliseconds)1732956789123
{nano}Current nanosecond time1732956789123456789
{class}Simplified class name (lowercase)myhandler
{parent}Parent actor ID (if hierarchical)parent-actor

Usage

// Simple sequence
Pid actor = system.actorOf(MyHandler.class)
.withIdTemplate("user-{seq}")
.spawn();
// Result: "user-1", "user-2", "user-3", ...

// Multiple placeholders
Pid actor = system.actorOf(MyHandler.class)
.withIdTemplate("{class}-{seq}-{short-uuid}")
.spawn();
// Result: "myhandler-1-a1b2c3d4"

// Template with timestamp
Pid actor = system.actorOf(MyHandler.class)
.withIdTemplate("session-{timestamp}-{seq}")
.spawn();
// Result: "session-1732956789123-1"

// Complex template
Pid actor = system.actorOf(MyHandler.class)
.withIdTemplate("{class}-{seq}-{uuid}-{timestamp}")
.spawn();
// Result: "myhandler-1-a1b2c3d4-e5f6-7890-abcd-ef1234567890-1732956789123"

Sequence Counters

  • {seq}: Global counter per template prefix

    • "user-{seq}"user-1, user-2, user-3
    • "order-{seq}"order-1, order-2, order-3
    • Different prefixes maintain separate counters
  • {template-seq}: Counter per exact template pattern

    • Same template = same counter
    • Different templates = different counters
// Separate counters for different templates
Pid user1 = system.actorOf(Handler.class)
.withIdTemplate("user-{seq}")
.spawn(); // "user-1"

Pid order1 = system.actorOf(Handler.class)
.withIdTemplate("order-{seq}")
.spawn(); // "order-1"

Pid user2 = system.actorOf(Handler.class)
.withIdTemplate("user-{seq}")
.spawn(); // "user-2"

Persistence Integration

🔄 Automatic Counter Recovery: When using sequence-based naming with stateful actors, Cajun automatically scans persisted actors on startup using PersistenceProvider.listPersistedActors() and initializes counters to prevent ID collisions.

// Setup: Register persistence provider
PersistenceProvider provider = new FileSystemPersistenceProvider(dataPath);

// First run: Create stateful actors with sequential IDs
// The system automatically generates IDs and creates persistence stores
Pid user1 = system.statefulActorOf(UserHandler.class, initialState)
.withIdStrategy(IdStrategy.CLASS_BASED_SEQUENTIAL)
.withPersistence(provider) // System uses generated ID for persistence
.spawn();
// Result: "userhandler:1"
// Persistence created: journal and snapshot for "userhandler:1"

Pid user2 = system.statefulActorOf(UserHandler.class, initialState)
.withIdStrategy(IdStrategy.CLASS_BASED_SEQUENTIAL)
.withPersistence(provider) // System uses generated ID for persistence
.spawn();
// Result: "userhandler:2"
// Persistence created: journal and snapshot for "userhandler:2"

// ============================================================
// Application restarts...
// ============================================================

// On startup, Cajun automatically:
// 1. Calls provider.listPersistedActors()
// 2. Finds: ["userhandler:1", "userhandler:2"]
// 3. Parses IDs and sets counter to 2
// 4. New actors will start from 3

// Create new actor after restart - counter resumes automatically!
Pid user3 = system.statefulActorOf(UserHandler.class, initialState)
.withIdStrategy(IdStrategy.CLASS_BASED_SEQUENTIAL)
.withPersistence(provider) // Same simple call
.spawn();
// Result: "userhandler:3" (not "userhandler:1"!)
// No collision with existing persisted actors

Key Point: You don't need to manually specify actor IDs in the persistence calls. The system:

  1. Generates the ID using your strategy (e.g., CLASS_BASED_SEQUENTIAL)
  2. Automatically creates journal and snapshot stores using that ID
  3. On restart, scans all persisted actors and resumes counters
  4. Ensures new actors never collide with existing ones

How It Works:

  1. On Startup: IdTemplateProcessor calls persistenceProvider.listPersistedActors()
  2. Parse IDs: Scans all persisted actor IDs for sequential patterns (e.g., prefix:number)
  3. Initialize Counters: Sets each counter to the maximum found sequence number
  4. Resume Sequence: New actors continue from the next available number

Supported Patterns:

The counter recovery works with IDs that follow the prefix:number pattern (with a colon):

// ✅ CLASS_BASED_SEQUENTIAL strategy (uses colon)
"userhandler:1", "userhandler:2", "userhandler:3"
// Counter initialized to 3

// ✅ Custom templates with colon separator
.withIdTemplate("user:{seq}") // Generates: "user:1", "user:2"
// Counter initialized to max found

// ✅ Hierarchical IDs with colon
"parent/child:1", "parent/child:2"
// Counter initialized to 2 (uses base ID after last '/')

// ❌ Templates with other separators NOT supported
.withIdTemplate("user-{seq}") // Generates: "user-1", "user-2"
// Counter recovery WILL NOT WORK - use explicit IDs or colon separator

Important: If you use templates with separators other than colons (e.g., "user-{seq}", "session_{seq}"), the counter recovery will not work. For persistence with templates, use:

  • "user:{seq}" instead of "user-{seq}"
  • "session:{seq}" instead of "session-{seq}"
  • Or use CLASS_BASED_SEQUENTIAL strategy ✅

Benefits:

  • No ID Collisions: New actors never reuse IDs of persisted actors
  • Automatic Recovery: No manual counter management required
  • Seamless Restarts: Actors can be stopped and restarted without ID conflicts
  • Predictable Behavior: Sequence continues naturally across restarts
  • Works with Hierarchies: Handles parent/child relationships correctly

Example: User Session Management

public class SessionManager {
private final ActorSystem system;
private final PersistenceProvider persistence;

public Pid createSession(String userId) {
// The ID template generates the actor ID automatically
// No need to manually track sequence numbers!
var builder = system.statefulActorOf(SessionHandler.class, new SessionState(userId))
.withIdStrategy(IdStrategy.CLASS_BASED_SEQUENTIAL);

// Get the generated ID to use for persistence
Pid pid = builder.spawn();
String actorId = pid.id();

// Now configure persistence with the actual ID
return system.statefulActorOf(SessionHandler.class, new SessionState(userId))
.withId(actorId) // Use the same ID
.withPersistence(
persistence.createMessageJournal(actorId),
persistence.createSnapshotStore(actorId)
)
.spawn();
}
}

// Better approach: Let the builder handle everything
public class SessionManager {
private final ActorSystem system;
private final PersistenceProvider persistence;

public Pid createSession(String userId) {
return system.statefulActorOf(SessionHandler.class, new SessionState(userId))
.withIdStrategy(IdStrategy.CLASS_BASED_SEQUENTIAL)
.withPersistence(persistence) // Provider creates journal/snapshot using actor ID
.spawn();
}
}

// First run:
// sessionhandler:1, sessionhandler:2, sessionhandler:3 created

// After restart:
// Cajun scans and finds: sessionhandler:1, sessionhandler:2, sessionhandler:3
// Counter initialized to 3
// Next session will be: sessionhandler:4

Pros and Cons

Pros:

  • ✅ Readable and meaningful IDs
  • ✅ Automatic uniqueness via counters
  • ✅ Flexible composition of information
  • ✅ Great for debugging and logging
  • Counters resume from persisted state (with persistence)

Cons:

  • ⚠️ Counters reset on restart for stateless actors
  • ⚠️ Slightly more overhead than strategies
  • ⚠️ Requires persistence provider for counter recovery

ID Strategies

Predefined strategies for common ID generation patterns. Best for:

  • Consistent ID generation across your application
  • When you don't need custom formatting
  • Maximum performance

Available Strategies

1. UUID (Default)

Generates a random UUID for each actor.

Pid actor = system.actorOf(MyHandler.class)
.withIdStrategy(IdStrategy.UUID)
.spawn();
// Result: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

Pros:

  • ✅ Guaranteed unique
  • ✅ No coordination needed
  • ✅ Works in distributed systems

Cons:

  • ⚠️ Not human-readable
  • ⚠️ Can't infer actor type from ID

2. CLASS_BASED_UUID

Combines class name with UUID: {class}:{uuid}

Pid actor = system.actorOf(MyHandler.class)
.withIdStrategy(IdStrategy.CLASS_BASED_UUID)
.spawn();
// Result: "myhandler:a1b2c3d4-e5f6-7890-abcd-ef1234567890"

Pros:

  • ✅ Unique and identifiable
  • ✅ Can filter by class in logs
  • ✅ Good for debugging

Cons:

  • ⚠️ Longer IDs
  • ⚠️ Still not very readable

3. CLASS_BASED_SEQUENTIAL

Combines class name with auto-incrementing counter: {class}:{seq}

Pid actor = system.actorOf(MyHandler.class)
.withIdStrategy(IdStrategy.CLASS_BASED_SEQUENTIAL)
.spawn();
// Result: "myhandler:1", "myhandler:2", "myhandler:3", ...

Pros:

  • ✅ Very readable
  • ✅ Short IDs
  • ✅ Easy to track actor count
  • ✅ Great for debugging
  • Counters resume from persisted state (with persistence)

Cons:

  • ⚠️ Counters reset on restart for stateless actors
  • ⚠️ Not suitable for distributed systems without coordination

💡 Tip: When using with stateful actors and persistence, counters automatically resume from the last persisted sequence number. See Persistence Integration for details.

4. SEQUENTIAL

Simple auto-incrementing counter.

Pid actor = system.actorOf(MyHandler.class)
.withIdStrategy(IdStrategy.SEQUENTIAL)
.spawn();
// Result: "1", "2", "3", ...

Pros:

  • ✅ Shortest possible IDs
  • ✅ Maximum performance

Cons:

  • ⚠️ No context about actor type
  • ⚠️ Only suitable for simple cases

Strategy Comparison

StrategyExample IDReadabilityUniquenessUse Case
UUIDa1b2...⭐⭐⭐⭐⭐Distributed systems
CLASS_BASED_UUIDhandler:a1b2...⭐⭐⭐⭐⭐⭐⭐Multi-class systems
CLASS_BASED_SEQUENTIALhandler:1⭐⭐⭐⭐⭐⭐⭐⭐Single-node apps
SEQUENTIAL1⭐⭐⭐⭐⭐⭐Simple testing

Hierarchical IDs

Create parent-child relationships with automatic ID prefixing.

Basic Hierarchy

// Create parent
Pid parent = system.actorOf(ParentHandler.class)
.withId("parent")
.spawn();

// Create child - ID is automatically prefixed
Pid child = system.actorOf(ChildHandler.class)
.withId("child")
.withParent(system.getActor(parent))
.spawn();
// Result: "parent/child"

Hierarchies with Templates

Pid parent = system.actorOf(ParentHandler.class)
.withId("parent")
.spawn();

// Children with sequential IDs
Pid child1 = system.actorOf(ChildHandler.class)
.withIdTemplate("child-{seq}")
.withParent(system.getActor(parent))
.spawn();
// Result: "parent/child-1"

Pid child2 = system.actorOf(ChildHandler.class)
.withIdTemplate("child-{seq}")
.withParent(system.getActor(parent))
.spawn();
// Result: "parent/child-2"

Hierarchies with Strategies

Pid parent = system.actorOf(ParentHandler.class)
.withId("parent")
.spawn();

Pid child = system.actorOf(ChildHandler.class)
.withIdStrategy(IdStrategy.CLASS_BASED_SEQUENTIAL)
.withParent(system.getActor(parent))
.spawn();
// Result: "parent/childhandler:1"

Deep Hierarchies

Pid grandparent = system.actorOf(Handler.class)
.withId("grandparent")
.spawn();

Pid parent = system.actorOf(Handler.class)
.withId("parent")
.withParent(system.getActor(grandparent))
.spawn();
// Result: "grandparent/parent"

Pid child = system.actorOf(Handler.class)
.withId("child")
.withParent(system.getActor(parent))
.spawn();
// Result: "grandparent/parent/child"

Using {parent} Placeholder

Pid parent = system.actorOf(ParentHandler.class)
.withId("parent")
.spawn();

Pid child = system.actorOf(ChildHandler.class)
.withIdTemplate("{parent}/child-{seq}")
.withParent(system.getActor(parent))
.spawn();
// Result: "parent/child-1"

Best Practices

1. Choose the Right Approach

// ✅ Good: Explicit IDs for singletons
Pid userService = system.actorOf(UserServiceHandler.class)
.withId("user-service")
.spawn();

// ✅ Good: Templates for dynamic actors
Pid session = system.actorOf(SessionHandler.class)
.withIdTemplate("session-{seq}")
.spawn();

// ✅ Good: Strategies for consistency
Pid worker = system.actorOf(WorkerHandler.class)
.withIdStrategy(IdStrategy.CLASS_BASED_SEQUENTIAL)
.spawn();

// ❌ Bad: Mixing approaches unnecessarily
Pid actor = system.actorOf(Handler.class)
.withId("actor")
.withIdTemplate("template-{seq}") // Ignored!
.withIdStrategy(IdStrategy.UUID) // Ignored!
.spawn();

2. Use Meaningful Names

// ✅ Good: Descriptive IDs
.withIdTemplate("user-session-{seq}")
.withIdTemplate("order-processor-{timestamp}")
.withIdTemplate("{class}-worker-{seq}")

// ❌ Bad: Generic IDs
.withIdTemplate("actor-{seq}")
.withIdTemplate("thing-{uuid}")

3. Consider Persistence

// ✅ Good: Explicit IDs for singleton stateful actors
Pid counter = system.statefulActorOf(CounterHandler.class, 0)
.withId("global-counter") // Same ID after restart
.withPersistence(...)
.spawn();

// ✅ Good: Sequential IDs with persistence (counters auto-resume)
Pid user = system.statefulActorOf(UserHandler.class, initialState)
.withIdStrategy(IdStrategy.CLASS_BASED_SEQUENTIAL)
.withPersistence(...)
.spawn();
// Result: "userhandler:1", "userhandler:2", ...
// After restart: counters resume, no collisions!

// ⚠️ Caution: Sequential IDs reset for stateless actors
Pid temp = system.actorOf(TempHandler.class)
.withIdTemplate("temp-{seq}") // Counter resets on restart!
.spawn();

Key Point: Sequential IDs ({seq}, CLASS_BASED_SEQUENTIAL) work seamlessly with persistence. Cajun automatically scans persisted actors on startup and resumes counters, preventing ID collisions. See Persistence Integration for details.

4. Hierarchies for Organization

// ✅ Good: Organize related actors
Pid service = system.actorOf(ServiceHandler.class)
.withId("user-service")
.spawn();

Pid cache = system.actorOf(CacheHandler.class)
.withId("cache")
.withParent(system.getActor(service))
.spawn();
// Result: "user-service/cache"

Pid db = system.actorOf(DbHandler.class)
.withId("database")
.withParent(system.getActor(service))
.spawn();
// Result: "user-service/database"

5. Logging and Debugging

// ✅ Good: IDs that help debugging
.withIdTemplate("{class}-{seq}-{short-uuid}")
// Result: "userhandler-1-a1b2c3d4"
// You can see: type, order, and unique identifier

// ✅ Good: Include context in IDs
.withIdTemplate("tenant-{tenant-id}-user-{seq}")
// Result: "tenant-123-user-1"

Examples

Example 1: User Session Management

public class SessionManager {
private final ActorSystem system;

public Pid createSession(String userId) {
return system.actorOf(SessionHandler.class)
.withIdTemplate("session-" + userId + "-{timestamp}")
.spawn();
// Result: "session-user123-1732956789123"
}
}

Example 2: Worker Pool

public class WorkerPool {
private final ActorSystem system;
private final List<Pid> workers = new ArrayList<>();

public void createWorkers(int count) {
for (int i = 0; i < count; i++) {
Pid worker = system.actorOf(WorkerHandler.class)
.withIdStrategy(IdStrategy.CLASS_BASED_SEQUENTIAL)
.spawn();
workers.add(worker);
}
// Result: "workerhandler:1", "workerhandler:2", ...
}
}

Example 3: Microservice Architecture

public class MicroserviceActors {
private final ActorSystem system;

public void setupServices() {
// API Gateway
Pid gateway = system.actorOf(GatewayHandler.class)
.withId("api-gateway")
.spawn();

// User Service with children
Pid userService = system.actorOf(UserServiceHandler.class)
.withId("user-service")
.spawn();

Pid userCache = system.actorOf(CacheHandler.class)
.withId("cache")
.withParent(system.getActor(userService))
.spawn();
// Result: "user-service/cache"

Pid userDb = system.actorOf(DbHandler.class)
.withId("database")
.withParent(system.getActor(userService))
.spawn();
// Result: "user-service/database"

// Order Service
Pid orderService = system.actorOf(OrderServiceHandler.class)
.withId("order-service")
.spawn();

// Dynamic order processors
for (int i = 0; i < 5; i++) {
system.actorOf(OrderProcessorHandler.class)
.withIdTemplate("processor-{seq}")
.withParent(system.getActor(orderService))
.spawn();
}
// Result: "order-service/processor-1", "order-service/processor-2", ...
}
}

Example 4: Testing with Predictable IDs

@Test
void testActorCommunication() {
ActorSystem system = new ActorSystem();

// Use explicit IDs for easy testing
Pid sender = system.actorOf(SenderHandler.class)
.withId("test-sender")
.spawn();

Pid receiver = system.actorOf(ReceiverHandler.class)
.withId("test-receiver")
.spawn();

// Easy to verify in logs and assertions
sender.tell(new SendTo("test-receiver", "Hello"));

// Can look up by ID
Actor<?> receiverActor = system.getActor(receiver);
assertNotNull(receiverActor);
}

Example 5: Multi-Tenant System

public class TenantManager {
private final ActorSystem system;

public Pid createTenantActor(String tenantId) {
// Tenant supervisor
Pid tenant = system.actorOf(TenantHandler.class)
.withId("tenant-" + tenantId)
.spawn();

// Tenant-specific workers
for (String service : List.of("auth", "data", "cache")) {
system.actorOf(ServiceHandler.class)
.withIdTemplate(service + "-{seq}")
.withParent(system.getActor(tenant))
.spawn();
}
// Result: "tenant-123/auth-1", "tenant-123/data-1", "tenant-123/cache-1"

return tenant;
}
}

Summary

Cajun's ID system provides flexibility for every use case:

  • Explicit IDs: For singletons and well-known actors
  • Templates: For readable, dynamic IDs with context
  • Strategies: For consistent, automatic ID generation
  • Hierarchies: For organizing related actors

Choose the approach that best fits your needs, and remember the priority system when combining multiple approaches.

For more information, see the Cajun GitHub repository for additional documentation and examples.