All skills
apollographql avatar

/rust-best-practices

@13ff457 official
by Apollo GraphQLapollographql/skills115 stars
13

Guide for writing idiomatic Rust code based on Apollo GraphQL's best practices handbook. Use this skill when: (1) writing new Rust code or functions, (2) reviewing or refactoring existing Rust code, (3) deciding between borrowing vs cloning or ownership patterns, (4) implementing error handling with Result types, (5) optimizing Rust code for performance, (6) writing tests or documentation for Rust projects.

Use this Skill: https://skilld.dev/gh/apollographql/skills/rust-best-practices

This session only. Nothing lands on disk.

referenceschapter_06.md

≈1.8k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Chapter 6 - Generics, Dynamic Dispatch and Static Dispatch

Static where you can, dynamic where you must

Rust allows you to handle polymorphic code in two ways:

  • Generics / Static Dispatch: compile-time, monomorphized per use.
  • Trait Objects / Dynamic Dispatch: runtime vtable, single implementation.

Understanding the trade-offs lets you write faster, smaller and more flexible code.

6.1 Generics

Every programming language has tools for effectively handling the duplication of concepts. In Rust, one such tool is generics: abstract stand-ins for concrete types or other properties. We can express the behavior of generics or how they relate to other generics without knowing what will be in their place when compiling and running the code.

We use generics to create definitions for items like function signatures or structs, which we can then use with many different concrete data types. Let's first look at how to define functions, structs, enums, and methods using generics. Generics can also be used to implement Type State Pattern and constrain a struct functionality to certain expected types, more on type state on chapter 7.

Generics by Examples.

Generics Performance

You might be wondering whether there is a runtime cost when using generic type parameters. The good news is that using generic types won't make your program run any slower than it would with concrete types. Rust accomplishes this by performing monomorphization of the code using generics at compile time. Monomorphization is the process of turning generic code into specific code by filling in the concrete types that are used when compiled. The compiler checks for all occurrences of the generic parameter and generates code for the concrete types the generic code is called with.

6.2 Static Dispatch: impl Trait or <T: Trait>

A static dispatch is basically a constrained version of generics, a trait bounded generic, at compile-time it is able to check if your generic satisfies the declared traits.

✅ Best when:

  • You want zero runtime cost, by paying the compile time cost.
  • You need tight loops or performance.
  • Your types are known at compile time.
  • You are working with single-use implementations (monomorphized).

🏎️ Example: High-performance function with generic

fn specialized_sum<U: Sum + RandomMapping, T: Iterator<Item = U>>(iter: T) -> U {
    iter.map(|x| x.random_mapping()).sum()
}

// or, equivalent, more modern
fn specialized_sum<U: Sum + RandomMapping>(iter: impl Iterator<Item = U>) -> U {
    iter.map(|x| x.random_mapping()).sum()
}

This is compiled into specialized machine code for each usage, fast and inlined.

6.3 Dynamic Dispatch: dyn Trait

Usually dynamic dispatch is used with some kind of pointer or a reference, like Box<dyn Trait>, Arc<dyn Trait> or &dyn trait.

✅ Best when:

  • You absolutely need runtime polymorphism.
  • You need to store different implementations in one collection.
  • You want to abstract internals behind a stable interface.
  • You are writing a plugin-style architecture.

❗ Closer to what you would get in an object oriented language and can have some heavy costs associated to it. Can avoid generics entirely and let you mix types that implement the same traits.

🚚 Example: Heterogeneous collection

trait Animal {
    fn greet(&self) -> String;
}

struct Dog;
impl Animal for Dog {
    fn greet(&self) -> String {
        "woof".to_string()
    }
}

struct Cat;
impl Animal for Cat {
    fn greet(&self) -> String {
        "meow".to_string()
    }
}

fn all_animals_greeting(animals: Vec<Box<dyn Animal>>) {
    for animal in animals {
        println!("{}", animal.greet())
    }
}

6.4 Trade-off summary

| | Static Dispatch (impl Trait) | Dynamic Dispatch (dyn Trait) | |------------------- |------------------------------ |---------------------------------- | | Performance | ✅ Faster, inlined | ❌ Slower: vtable indirection | | Compile time | ❌ Slower: monomorphization | ✅ Faster: shared code | | Binary size | ❌ Larger: per-type codegen | ✅ Smaller | | Flexibility | ❌ Rigid, one type at a time | ✅ Can mix types in collections | | Use in trait fn() | ❌ Traits must be object-safe | ✅ Works with trait objects | | Errors | ✅ Clearer | ❌ Erased types can confuse errors |

  • Prefer generics/static dispatch when you control the call site and want performance.
  • Use dynamic dispatch when you need abstraction, plugins or mixed types. 🚨 Runtime cost.
  • If you are not sure, start with generics, trait bound them - then use Box<dyn Trait> when flexibility outweighs speed.

Favor static dispatch until your trait needs to live behind a pointer.

6.5 Best Practices for Dynamic Dispatch

Dynamic dispatch Ptr<dyn Trait> is a powerful tool, but it also has significant performance trade-offs. You should only reach for it when type erasure or runtime polymorphism are essential. It is important to know when you need Trait Objects:

✅ Use Dynamic Dispatch When:

  • You need heterogeneous types in a collection:
fn all_animals_greeting(animals: Vec<Box<dyn Animal>>) {
    for animal in animals {
        println!("{}", animal.greet())
    }
}
  • You want runtime plugins or hot-swappable components.
  • You want to abstract internals from the caller (library design).

❌ Avoid Dynamic Dispatch When:

  • You control the concrete types.
  • You are writing code in performance critical paths.
  • You can express the same logic in other ways while keeping simplicity, e.g. generics.

6.6 🚨 Trait Objects Ergonomics

  • Prefer &dyn Trait over Box<dyn Trait> when you don't need ownership.
  • Use Arc<dyn Trait + Send + Sync> for shared access across threads.
  • Don't use dyn Trait if the trait has methods that return Self.
  • Avoid boxing too early. Don't box inside structs unless you are sure it'll be beneficial or is required (recursive).
// ✅ Use generics when possible
struct Renderer<B: RenderBackend> {
    backend: B
}

// ❌ Premature Boxing
struct Renderer {
    backend: Box<dyn RenderBackend> // Boxing too early
}
  • If you must expose a dyn trait in a public API, Box at the boundary, not internally.
  • Object Safety: You can only create dyn Traits from object-safe traits:
    • It has no generic methods.
    • It doesn't require Self: Sized.
    • All method signatures use &self, &mut self or self.
    // ✅ Object Safe
    trait Runnable {
        fn run(&self);
    }
    
    // ❌ Not Object Safe
    trait Factory {
        fn create<T>() -> T; // generic methods are not allowed
    }

Source: SKILL.md on GitHub

No alerts3d5 checks · Risk SAFE
  • Gen Agent Trust Hub3d

    This skill provides a comprehensive guide to idiomatic Rust development based on Apollo GraphQL's best practices. It covers coding styles, error handling, performance optimization, and testing procedures using standard ecosystem tools.

  • Socket3d

    No alerts

  • Snyk3d

    Risk: LOW · No issues

  • Runlayer6mo

    1/10 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 13ff457. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 3 days ago
What it can do
Runs commands
compatibility
Rust 1.70+, Cargo
metadata
{
  "author": "apollographql",
  "version": "1.1.2"
}
All 1 allowed tools
Bash(cargo:*) Bash(rustc:*) Bash(rustfmt:*) Bash(clippy:*) Read Write Edit Glob Grep

README badge

README badge for apollographql/skills/rust-best-practices

Guides idiomatic Rust code review and writing using Apollo GraphQL's best practices handbook, covering ownership patterns, error handling, performance profiling, testing conventions, and type-state patterns. Use when writing new Rust, refactoring existing code, deciding between borrowing vs cloning, or implementing error handling and performance optimizations.

Generated from the current SKILL.md.

Does this skill cover async Rust patterns?
The skill references Apollo's best practices handbook which covers core ownership, borrowing, error handling, testing, and type patterns, but async-specific guidance is not listed in the chapter references provided.
Can I use this skill to lint my existing codebase?
Yes. The skill includes Clippy configuration and linting best practices, with specific commands like `cargo clippy --all-targets --all-features --locked -- -D warnings` to identify issues in your code.
Does this skill recommend anyhow or thiserror for error handling?
Yes. The skill specifies using `thiserror` for library errors and `anyhow` for binaries only, and advises never using `unwrap()` or `expect()` outside tests.
What Rust version does this require?
The skill requires Rust 1.70 or later and Cargo.
Does this cover the type state pattern?
Yes. Chapter 7 covers the type state pattern for encoding valid states in the type system to catch invalid operations at compile time, with a code example showing Connection states.

Generated from the current SKILL.md. These answers refresh after source changes.