singleton-registry
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.
Installation
Add this to your Cargo.toml:
[]
= "2.1.1"
Features & Design
- 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
Design Philosophy
This crate implements a contract-based service locator pattern:
Contracts as Traits
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.
Singleton Replacement & Arc Safety
When you re-register a type, the registry atomically replaces the stored Arc. However, existing references remain valid:
let old_ref: = get.unwrap; // Holds Arc clone
register; // Registry updated
// old_ref still works - it holds the previous Arc until dropped
let new_ref: = get.unwrap; // Gets new instance
This enables runtime replacement (e.g., hot-swapping configurations) without breaking in-flight operations.
Unit Testing Without Mocking Libraries
Register mock implementations during test setup:
Enforcing Good Architecture
The registry pattern encourages:
- Interface segregation: Define focused contracts (traits)
- Dependency inversion: Depend on abstractions, not concretions
- Single responsibility: Each singleton handles one concern
Quick Start
use define_registry;
use Arc;
define_registry!;
define_registry!;
register;
register;
let message: = get.unwrap;
let number: = get.unwrap;
assert_eq!;
assert_eq!;
Advanced Usage
use define_registry;
use Arc;
define_registry!;
set_trace_callback;
register;
register;
let multiply_by_two: fn = ;
register;
assert!;
let number: = get.unwrap;
let config: = get.unwrap;
let doubler: = get.unwrap;
let result = doubler;
assert_eq!;
assert_eq!;
assert_eq!;
Multiple Isolated Registries
use define_registry;
define_registry!;
define_registry!;
define_registry!;
register;
register;
register;
let db_conn = .unwrap;
let cache_conn = .unwrap;
API Reference
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
Error Handling
All fallible operations return Result<T, RegistryError>:
Example:
use define_registry;
define_registry!;
// Handle errors explicitly
match
// Or use ? operator
Note on Lock Poisoning: The registry automatically recovers from poisoned locks by extracting the inner value. This is safe because registry operations are idempotent.
Use Cases
- 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.
Roadmap
Future Considerations
get_or_default()- Convenience method with fallback valuesget_or_init()- Lazy initialization supportregister_if_absent()- Conditional registration
Non-Goals
- 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.
Running Examples
The examples/ directory contains runnable demonstrations:
# Basic usage: primitives, structs, get(), get_cloned(), contains()
# Trait contracts: register and retrieve trait objects
# Singleton replacement: Arc reference safety during runtime swaps
Porting to Other Languages
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.
License
BSD-3-Clause