Course contentsShow
← Rust
Lesson 650 of 1,9105. Type System and TraitsFree lesson

Building Robust Component Registries

Manage system-wide services using trait-based, type-safe lookup tables.

In this lesson, you’ll learn how to build a centralized, type-safe registry to manage system-wide services without relying on loose, error-prone global variables.

The Problem: Global Chaos

In large modular systems, components often need to access shared services like a Logger, a Database, or a Configuration manager. If you pass these dependencies manually through every function signature, you end up with "prop drilling." Conversely, using raw global variables is unsafe and difficult to test. A Component Registry acts as a central hub where components can be registered and retrieved by their trait interfaces, ensuring type safety throughout the system.

Implementing the Registry

The secret to a robust registry is using Any for type-erasure combined with TypeId to ensure that when you request a service, you get exactly the implementation you expect.

use std::any::{Any, TypeId};
use std::collections::HashMap;

pub struct ComponentRegistry {
    services: HashMap<TypeId, Box<dyn Any>>,
}

impl ComponentRegistry {
    pub fn new() -> Self {
        Self { services: HashMap::new() }
    }

    pub fn register<T: 'static>(&mut self, service: T) {
        self.services.insert(TypeId::of::<T>(), Box::new(service));
    }

    pub fn get<T: 'static>(&self) -> Option<&T> {
        self.services.get(&TypeId::of::<T>())
            .and_then(|s| s.downcast_ref::<T>())
    }
}

How It Works

  1. Type Erasure: We store services as Box<dyn Any>. Since the registry doesn't know the concrete types ahead of time, this allows us to put anything that satisfies 'static into the map.
  2. Type Identification: By using TypeId::of::<T>() as the key, we map the specific type to its implementation. This prevents collisions and allows us to enforce that the type requested is the type returned.
  3. Downcasting: When calling get, we use downcast_ref. This safely verifies that the internal type matches the requested type before returning it. If the service isn't registered, it simply returns None, allowing your system to handle missing dependencies gracefully.

This approach keeps your system architecture modular. You can swap out a logging implementation during tests without changing the code that consumes it, as long as both implementations satisfy the same trait interface.

Key Takeaway: Use a HashMap of TypeId to Box<dyn Any> to create a type-safe central hub that decouples components from their concrete dependencies.