A lightweight framework for building modular, interconnected applications in Rust.
Overwatch simplifies the development of complex systems by enabling seamless communication between independent components. Think of it as a lightweight alternative to microservices that runs within a single process.
| Traditional Approach | With Overwatch |
|---|---|
| Tightly coupled components | 🔌 Modular, independent services |
| Complex inter-process communication | 📨 Built-in async message passing |
| Manual lifecycle management | ⚡ Automatic service orchestration |
| Scattered configuration | ⚙️ Centralized settings management |
| Difficult to test | 🧪 Easy to mock and test services |
Overwatch uses a mediator pattern where the OverwatchRunner acts as the central coordinator for all services:
┌─────────────────────────────────────────────────────────────────────────────┐
│ OVERWATCH RUNNER │
│ (Central Coordinator) │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ MESSAGE RELAY │ │
│ │ Async communication between services │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ | │ │ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Service A │ <--> │ Service B │ <--> │ Service C │ │
│ │ │ │ │ │ │ │
│ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │
│ │ │Settings │ │ │ │Settings │ │ │ │Settings │ │ │
│ │ └─────────┘ │ │ └─────────┘ │ │ └─────────┘ │ │
│ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │
│ │ │ State │ │ │ │ State │ │ │ │ State │ │ │
│ │ └─────────┘ │ │ └─────────┘ │ │ └─────────┘ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ LIFECYCLE MANAGEMENT │ │
│ │ Start • Stop • Restart • Configuration Updates │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
| Concept | Description |
|---|---|
| OverwatchRunner | The central coordinator that manages all services |
| Service | An independent unit of work with its own lifecycle |
| Relay | Type-safe async channel for inter-service communication |
| Settings | Configuration for each service |
| State | Persistent state that survives restarts |
| StateOperator | Logic for loading/saving service state |
- Rust ≥ 1.63
Add the following to your Cargo.toml:
[dependencies]
overwatch = "1"
overwatch-derive = "1"
async-trait = "0.1"
tokio = { version = "1", features = ["full"] }Here's the simplest possible Overwatch application:
use async_trait::async_trait;
use overwatch::{
derive_services,
overwatch::OverwatchRunner,
services::{
ServiceCore, ServiceData,
state::{NoOperator, NoState},
},
DynError, OpaqueServiceResourcesHandle,
};
// 1️⃣ Define your service
struct HelloService {
handle: OpaqueServiceResourcesHandle<Self, RuntimeServiceId>,
}
// 2️⃣ Specify service data types
impl ServiceData for HelloService {
type Settings = (); // No configuration needed
type State = NoState<Self::Settings>; // No persistent state
type StateOperator = NoOperator<Self::State>; // No state operations
type Message = (); // No incoming messages
}
// 3️⃣ Implement the service logic
#[async_trait]
impl ServiceCore<RuntimeServiceId> for HelloService {
fn init(
handle: OpaqueServiceResourcesHandle<Self, RuntimeServiceId>,
_state: Self::State,
) -> Result<Self, DynError> {
Ok(Self { handle })
}
async fn run(self) -> Result<(), DynError> {
println!("👋 Hello from Overwatch!");
// Signal that this service is done. We can shut down the whole application.
self.handle
.overwatch_handle
.shutdown()
.await;
Ok(())
}
}
// 4️⃣ Compose your application
#[derive_services]
struct MyApp {
hello: HelloService,
}
// 5️⃣ Run it!
fn main() {
let settings = MyAppServiceSettings { hello: () };
let app = OverwatchRunner::<MyApp>::run(settings, None)
.expect("Failed to start");
// Start all services
app.runtime()
.handle()
.block_on(app.handle().start_all_services())
.expect("Failed to start services");
app.blocking_wait_finished();
}Services communicate through typed message relays:
┌──────────────┐ PongMessage ┌──────────────┐
│ │ ───────────────────────────> │ │
│ Ping Service │ │ Pong Service │
│ │ <─────────────────────────── │ │
└──────────────┘ PingMessage └──────────────┘
// Define message types
#[derive(Debug)]
enum PingMessage { Pong }
#[derive(Debug)]
enum PongMessage { Ping }
// In PingService::run()
async fn run(self) -> Result<(), DynError> {
// Get a relay to send messages to PongService
let pong_relay = self.handle
.overwatch_handle
.relay::<PongService>()
.await?;
// Send a message
pong_relay.send(PongMessage::Ping).await?;
// Receive messages
while let Some(msg) = self.handle.inbound_relay.recv().await {
match msg {
PingMessage::Pong => println!("Received Pong!"),
}
}
Ok(())
}The examples/ping_pong directory contains a complete working example demonstrating:
- ✅ Service definition and registration
- ✅ Inter-service messaging via relays
- ✅ Settings configuration
- ✅ State persistence and restoration
- ✅ Custom state operators
Run it:
cargo run --example ping_pongWhat it does:
- Ping sends a message to Pong every second
- Pong receives it and replies back
- Ping tracks the count and persists it to disk
- After 30 pongs, the application exits
| Resource | Description |
|---|---|
| API Docs | Full API reference |
| Examples | Working code examples |
| CONTRIBUTING.md | Contribution guidelines |
Overwatch/
├── overwatch/ # Core framework library
│ └── src/
│ ├── overwatch/ # Runner, handle, commands
│ ├── services/ # Service traits and utilities
│ └── utils/ # Helper utilities
├── overwatch-derive/ # Procedural macros (#[derive_services])
└── examples/
└── ping_pong/ # Complete working example
# Run all tests
cargo test
# Run with output
cargo test -- --nocapturecargo run --example ping_pongcargo doc --open --no-depsWe welcome contributions! Please read our Contributing Guidelines for details.
Dual-licensed under Apache 2.0 and MIT.
Join the conversation: