A thread-safe singleton registry for Rust.
Create isolated registries for storing and retrieving any type.
Each type can have only one instance per registry.
Add this to your Cargo.toml:
[dependencies]
singleton-registry = "2.1.2"- Synchronous API: No async/await complexity - simple function calls
- Thread-safe: All operations safe across multiple threads using
ArcandMutex - Isolated registries: Create multiple independent registries with
define_registry!- no hidden globals - True singleton: Only one instance per type per registry
- Write-once pattern: Designed for initialization-time registration with optional runtime overrides
- No removal: Values can be overridden but not removed - provide default values for fail-safe operation
- Override-friendly: Later registrations replace previous ones
- Tracing support: Optional callback system for monitoring operations
This crate implements a service locator pattern in the sense Martin Fowler defined in his 2004 article Inversion of Control Containers and the Dependency Injection Pattern β extended with explicit contracts as Rust traits.
Fowler's known trade-off applies: every caller has a dependency on the locator itself, and the dependencies of a component are not visible from its signature. This crate accepts that trade-off deliberately β the benefit is that a component can ask for a capability that may not exist yet and degrade gracefully (via try_get) rather than failing at construction time.
A contract is a trait (interface) that defines the API a singleton must fulfill. By registering trait objects (Arc<dyn MyTrait>), you decouple consumers from concrete implementations. Any part of your system can request the contract without knowing which implementation backs it.
When you re-register a type, the registry atomically replaces the stored Arc. However, existing references remain valid:
let old_ref: Arc<MyService> = registry::get().unwrap(); // Holds Arc clone
registry::register(new_service); // Registry updated
// old_ref still works - it holds the previous Arc until dropped
let new_ref: Arc<MyService> = registry::get().unwrap(); // Gets new instanceThis enables runtime replacement (e.g., hot-swapping configurations) without breaking in-flight operations.
Register mock implementations during test setup:
#[cfg(test)]
mod tests {
use super::*;
define_registry!(test_registry);
#[test]
fn test_with_mock() {
test_registry::register(Arc::new(MockService) as Arc<dyn ServiceContract>);
// Test code uses test_registry::get() - no external mocking crate needed
}
}The registry pattern encourages:
- Interface segregation: Define focused contracts (traits)
- Dependency inversion: Depend on abstractions, not concretions
- Single responsibility: Each singleton handles one concern
use singleton_registry::define_registry;
use std::sync::Arc;
define_registry!(global);
define_registry!(cache);
global::register("Hello, World!".to_string());
cache::register(42i32);
let message: Arc<String> = global::get().unwrap();
let number: Arc<i32> = cache::get().unwrap();
assert_eq!(&*message, "Hello, World!");
assert_eq!(*number, 42);use singleton_registry::define_registry;
use std::sync::Arc;
define_registry!(app);
app::set_trace_callback(|event| {
println!("Registry event: {}", event);
});
app::register(12i32);
app::register("config".to_string());
let multiply_by_two: fn(i32) -> i32 = |x| x * 2;
app::register(multiply_by_two);
assert!(app::contains::<i32>().unwrap());
let number: Arc<i32> = app::get().unwrap();
let config: Arc<String> = app::get().unwrap();
let doubler: Arc<fn(i32) -> i32> = app::get().unwrap();
let result = doubler(21);
assert_eq!(result, 42);
assert_eq!(*number, 12);
assert_eq!(&*config, "config");use singleton_registry::define_registry;
define_registry!(database);
define_registry!(cache);
define_registry!(config);
database::register("postgresql://localhost".to_string());
cache::register("redis://localhost".to_string());
config::register("app_config".to_string());
let db_conn = database::get::<String>().unwrap();
let cache_conn = cache::get::<String>().unwrap();Each registry created with define_registry!(name) provides:
name::register(value)- Register a valuename::register_arc(arc_value)- Register an Arc-wrapped valuename::get::<T>()- Retrieve a value asArc<T>(returnsResult)name::try_get::<T>()- Retrieve a value asOption<Arc<T>>(returnsNoneinstead ofErr)name::get_cloned::<T>()- Retrieve a cloned value (requiresClone, returnsResult)name::contains::<T>()- Check if a type is registered (returnsResult)name::set_trace_callback(callback)- Set up tracingname::clear_trace_callback()- Clear tracing
All fallible operations return Result<T, RegistryError>:
pub enum RegistryError {
/// Type not found in the registry
TypeNotFound { type_name: &'static str },
/// Type mismatch during retrieval (should never happen)
TypeMismatch { type_name: &'static str },
/// Failed to acquire registry lock (automatically recovered)
RegistryLock,
}Example:
use singleton_registry::define_registry;
define_registry!(app);
// Handle errors explicitly
match app::get::<String>() {
Ok(value) => println!("Found: {}", value),
Err(e) => eprintln!("Error: {}", e), // "Type not found in registry: alloc::string::String"
}
// Or use ? operator
fn get_config() -> Result<std::sync::Arc<String>, singleton_registry::RegistryError> {
app::get::<String>()
}Note on Lock Poisoning: The registry automatically recovers from poisoned locks by extracting the inner value. This is safe because registry operations are idempotent.
- Application singletons (Config, Logger, DatabasePool, etc.)
- Isolated contexts (per-module registries, test isolation)
- Function helpers and utility closures
- Shared resources and components
- Service locator pattern with type safety
Best Practice: Register all required types during initialization-time to ensure get() never fails during runtime.
get_or_default()- Convenience method with fallback valuesget_or_init()- Lazy initialization supportregister_if_absent()- Conditional registration
- Async support (keeping it synchronous by design)
- Removal operations β override with a null object (a no-op implementation satisfying the same trait contract) to safely "disable" a registered value without risking a missing-type panic at call sites
See CHANGELOG.md for version history and CONTRIBUTING.md for contribution guidelines.
The examples/ directory contains runnable demonstrations:
# Basic usage: primitives, structs, get(), get_cloned(), contains()
cargo run --example basic_usage
# Trait contracts: register and retrieve trait objects
cargo run --example trait_contracts
# Singleton replacement: Arc reference safety during runtime swaps
cargo run --example singleton_replacementThis crate is the core building block of the JigsawFlow Microkernel pattern β a capability-driven architecture for offline-first, hot-swappable, language-agnostic applications. The registry is what makes the pattern possible: everything else in JigsawFlow is built on top of it. It is fully usable standalone.
For implementing a similar registry in TypeScript, C++, or another language, porting guidance has been consolidated into the JigsawFlow PLAN.md (Sections 2 and 11) β covering language-agnostic API surface, token design, thread safety, and reference counting across all ports.
BSD-3-Clause