How to Implement the Factory Pattern in Rust

Implement the Factory Pattern in Rust by defining a trait, creating concrete implementations, and using a function to return the appropriate type.

The problem with returning different types

You are building a game engine. The level editor exports a configuration file. One line says spawn: goblin. Another says spawn: dragon. Your code reads the file and needs to create the right enemy. In Python, you would return the class instance and move on. In Rust, the compiler stops you immediately.

The issue is size. A Goblin struct might hold a name and a health value. A Dragon struct might hold a name, health, a fire breath cooldown, and a list of hoarded items. These types have different sizes in memory. If your factory function promises to return a value, the compiler must reserve space on the stack for the return type. It cannot reserve space for "either a Goblin or a Dragon" because it doesn't know which one is coming, and it cannot resize the stack frame at runtime. The function signature must declare a single, fixed size.

This constraint breaks the abstraction you want. You don't want a separate function for every enemy. You want one factory that returns "something that acts like an enemy." Rust solves this by returning a pointer instead of the value itself. The pointer has a fixed size. The value it points to can be anything, as long as it lives on the heap.

Trait objects and the factory solution

The factory pattern in Rust relies on trait objects. A trait object lets you erase the concrete type and work with the behavior defined by a trait. The syntax is dyn Trait. You almost always wrap a trait object in a smart pointer like Box. Box<dyn Trait> is the standard way to return heterogeneous types from a factory.

Think of a universal remote control. The remote has buttons for "Volume Up" and "Power." It doesn't care if the device is a TV, a soundbar, or a projector. It just sends a signal. The device receives the signal and does the right thing. The remote is the Box<dyn Trait>. The device is the concrete struct. The protocol is the trait. The remote works with any device that speaks the protocol, even if the devices are completely different underneath.

In Rust, the dyn keyword tells the compiler to use dynamic dispatch. The compiler generates a virtual function table (vtable) for the trait. At runtime, the code looks up the function in the vtable to find the correct implementation. This allows the factory to return different types through the same interface.

Minimal example

Here is a factory that creates enemies based on a string identifier. The trait defines the contract. The structs implement the contract. The factory returns a Box<dyn Enemy>.

/// Defines the behavior all enemies must support.
trait Enemy {
    fn attack(&self);
    fn take_damage(&mut self, amount: u32);
}

/// A simple enemy with low health.
struct Goblin {
    health: u32,
}

impl Enemy for Goblin {
    fn attack(&self) {
        println!("Goblin bites for 1 damage.");
    }

    fn take_damage(&mut self, amount: u32) {
        self.health = self.health.saturating_sub(amount);
        println!("Goblin takes {} damage. Health: {}", amount, self.health);
    }
}

/// A powerful enemy with fire breath.
struct Dragon {
    health: u32,
    fire_cooldown: u32,
}

impl Enemy for Dragon {
    fn attack(&self) {
        if self.fire_cooldown == 0 {
            println!("Dragon breathes fire for 10 damage!");
        } else {
            println!("Dragon is cooling down.");
        }
    }

    fn take_damage(&mut self, amount: u32) {
        self.health = self.health.saturating_sub(amount);
        println!("Dragon takes {} damage. Health: {}", amount, self.health);
    }
}

/// Factory function that returns a boxed trait object.
/// The return type is fixed size (a pointer), but the data behind it varies.
fn create_enemy(kind: &str) -> Box<dyn Enemy> {
    match kind {
        "goblin" => Box::new(Goblin { health: 10 }),
        "dragon" => Box::new(Dragon { health: 100, fire_cooldown: 0 }),
        _ => panic!("Unknown enemy type: {}", kind),
    }
}

fn main() {
    // The factory decides the concrete type at runtime.
    let mut enemy = create_enemy("dragon");
    
    // We call trait methods without knowing the concrete type.
    enemy.attack();
    enemy.take_damage(15);
}

Under the hood: vtables and fat pointers

When you call create_enemy, the function allocates memory on the heap for the concrete struct. It wraps that memory in a Box. The Box is not just a pointer to the data. It is a "fat pointer" containing two parts: a pointer to the data and a pointer to the vtable.

The vtable is a compiler-generated table of function pointers. For the Enemy trait, the vtable contains the addresses of attack, take_damage, and the drop glue (the code that cleans up the struct). When you call enemy.attack(), the runtime code follows the vtable pointer, finds the entry for attack, and jumps to the correct implementation. If the enemy is a Dragon, it jumps to Dragon::attack. If it is a Goblin, it jumps to Goblin::attack.

This indirection is the cost of flexibility. Every method call on a trait object requires a pointer dereference and a vtable lookup. The compiler cannot inline these calls because it doesn't know the type at compile time. The performance impact is usually negligible for logic-heavy code, but it matters in tight loops.

The fat pointer also ensures safety. When the Box goes out of scope, the destructor uses the vtable to find the correct drop implementation. It knows exactly how to clean up a Dragon or a Goblin, even though the code only sees a Box<dyn Enemy>. Memory leaks are impossible as long as the trait object is properly dropped.

Realistic example: A component factory

Real applications often need factories that construct objects with data. A UI framework might parse a JSON description and build components. The factory needs to pass arguments to the constructors.

/// Trait for UI components.
/// All components can render themselves.
trait Component {
    fn render(&self);
}

/// A button component with a label.
struct Button {
    label: String,
}

impl Component for Button {
    fn render(&self) {
        println!("[Button: {}]", self.label);
    }
}

/// A text label component.
struct Label {
    text: String,
}

impl Component for Label {
    fn render(&self) {
        println!("Label: {}", self.text);
    }
}

/// Factory that builds components from a type string and content.
/// Returns a Box<dyn Component> to hide the concrete type.
fn build_component(kind: &str, content: &str) -> Box<dyn Component> {
    match kind {
        "button" => Box::new(Button {
            label: content.to_string(),
        }),
        "label" => Box::new(Label {
            text: content.to_string(),
        }),
        _ => panic!("Unsupported component: {}", kind),
    }
}

fn main() {
    // Simulate parsing a config or JSON.
    let components = vec![
        ("button", "Submit"),
        ("label", "Welcome"),
        ("button", "Cancel"),
    ];

    // Store heterogeneous components in a single vector.
    let ui: Vec<Box<dyn Component>> = components
        .iter()
        .map(|(kind, content)| build_component(kind, content))
        .collect();

    // Render all components uniformly.
    for comp in &ui {
        comp.render();
    }
}

The convention in Rust is to use Box::new explicitly in the factory. Some developers write Box::new(...) while others might try to rely on coercion. The explicit form is clearer and avoids confusion with Rc or Arc if the return type changes later. Also, always use dyn in the return type. Rust 2021 requires dyn for trait objects. Older code might show Box<Component>, but that syntax is deprecated and will not compile in modern editions.

Pitfalls: Object safety and performance

Not every trait can become a trait object. The compiler enforces "object safety." If a trait has methods that return Self, take Self as an argument, or use generic type parameters, you cannot create a dyn Trait. The compiler needs to know the size of Self to generate the vtable, and Self is unknown for a trait object.

If you try to make a trait object from an unsafe trait, the compiler rejects it with E0038 (the trait cannot be made into an object). For example, a trait with fn clone(&self) -> Self is not object-safe. The return type Self has unknown size. You cannot put a Box<dyn Widget> in a vtable entry for clone because the function would need to return a value of unknown size.

To fix this, refactor the trait. Use associated functions for construction. Use Box<Self> if you need to return a new instance dynamically. Or use a separate trait for the factory methods. Object safety is the gatekeeper. If the trait isn't safe, the factory can't build it.

Performance is the other consideration. Dynamic dispatch adds overhead. If you are calling a factory method millions of times per second, the vtable lookup and heap allocation will show up in profiling. In those cases, consider static dispatch with enums or generics. The factory pattern trades a small amount of speed for the ability to mix types in collections and extend behavior without recompiling the core library.

Dynamic dispatch buys flexibility. It costs a tiny bit of speed and a heap allocation. Know the trade-off.

Decision matrix

Use Box<dyn Trait> for the factory pattern when you need to return different concrete types from a single function and store them together in a collection. Use a closed enum when your types are fixed and you want the performance of static dispatch without heap allocation. Use generics when the type is known at the call site and you want the compiler to generate specialized code for each usage.

Use Box<dyn Trait> when the set of types is open-ended, such as plugins or user-defined extensions. Use an enum when you control all the variants and want exhaustive matching. Use generics when the caller provides the type and you want zero-cost abstraction.

Where to go next