Reactive actors for Rust.

Build type-safe, async-first reactive systems using an actor-based architecture with message passing and pub/sub.

main.rs
Cargo.toml
use acton_reactive::prelude::*;
#[acton_actor]
struct Counter { count: u32 }
#[acton_message]
struct Increment(u32);
#[acton_main]
async fn main() {
let mut app = ActonApp::launch_async().await;
let mut actor = app.new_actor::<Counter>();
actor.mutate_on::<Increment>(|actor, ctx| {
actor.model.count += ctx.message().0;
Reply::ready()
});
let handle = actor.start().await;
handle.send(Increment(1)).await;
}

Getting started

Build concurrent Rust apps without locks, race conditions, or shared state nightmares.

Acton gives you independent workers that own their state privately and communicate through messages. If you can write async Rust, you can write actors—and your concurrent code will be correct by construction.

Quick Start

Get running in 5 minutes.

Core Concepts

Understand actors, messages, and handlers.

Building Apps

Practical patterns for real applications.

API Reference

Quick reference for types and traits.


Choose your path

New to concurrent programming?

Start with What are Actors? to understand the concepts, then work through the Quick Start.

Experienced with actors?

Jump straight to Installation and Your First Actor. Check the Cheatsheet for quick patterns.


Actors eliminate data races by making isolation the default

Traditional concurrent Rust requires careful lock management:

// Shared state protected by locks...
let counter = Arc::new(Mutex::new(0));
let counter_clone = counter.clone();

// Spawn threads...
let handle = thread::spawn(move || {
    let mut num = counter_clone.lock().unwrap(); // Hope this doesn't deadlock!
    *num += 1;
});

// Hope you got all the Arc<Mutex<...>> right...

The problems: Lock contention, deadlocks, shared state bugs, and subtle race conditions that only appear in production.

Acton's solution: Each actor owns its data privately. No locks needed.

#[acton_actor]
struct Counter { count: i32 }

#[acton_message]
struct Increment;

builder.mutate_on::<Increment>(|actor, _msg| {
    actor.model.count += 1;
    Reply::ready()
});

handle.send(Increment).await;

What you get: No locks, isolated failures, guaranteed message ordering, and compile-time safety. Data races become impossible because actors never share mutable state.


A complete example in 20 lines

use acton_reactive::prelude::*;

#[acton_actor]
struct Counter { count: i32 }

#[acton_message]
struct Increment;

#[acton_main]
async fn main() {
    let mut app = ActonApp::launch();

    let handle = app
        .new_actor::<Counter>()
        .mutate_on::<Increment>(|actor, _| {
            actor.model.count += 1;
            println!("Count: {}", actor.model.count);
            Reply::ready()
        })
        .start()
        .await;

    handle.send(Increment).await.ok();
    handle.send(Increment).await.ok();
    handle.send(Increment).await.ok();

    app.shutdown_all().await.ok();
}

Output:

Count: 1
Count: 2
Count: 3

Each message is processed in order. The actor's state is never accessed concurrently. Ready to build? Start with Installation.


Two handler types cover every use case

If you need to...Use thisWhy
Modify the actor's datamutate_onExclusive access, one message at a time
Read the actor's dataact_onMultiple reads can run concurrently
// For mutations - one at a time, exclusive access
builder.mutate_on::<Increment>(|actor, _| {
    actor.model.count += 1;
    Reply::ready()
});

// For queries - can run concurrently with other reads
builder.act_on::<GetCount>(|actor, _| {
    Reply::with(actor.model.count)
});

When in doubt, use mutate_on

It's safer because it processes one message at a time. Optimize to act_on when you're sure the handler only reads.

See Messages & Handlers for the complete picture.


Current status

Pre-1.0 Software

acton-reactive is under active development. The API is stabilizing but may change before 1.0. Breaking changes bump the minor version (e.g., 0.7 → 0.8).


Start building now