What a sealed trait is

A sealed trait is a public trait that inherits from a private trait. Because the private trait is not accessible outside the crate, external code cannot implement the public trait either.

The core idea is simple:

  • expose the trait for method calls and bounds
  • hide the “implementation permission” behind a private module
  • keep control over which types can implement the trait

This is not a language feature; it is a design pattern built from Rust’s visibility rules.

Why it matters

Without sealing, a public trait becomes a contract with the entire ecosystem. Any future change can become a breaking change if someone has implemented that trait in a way your new design does not support.

Sealing is useful when:

  • you want to define a closed set of implementors
  • you expect to add methods later
  • you need to preserve internal invariants
  • you are modeling a protocol, state machine, or backend abstraction
  • you want to provide extension points only through controlled wrappers

The basic pattern

Here is the canonical structure:

mod sealed {
    pub trait Sealed {}
}

pub trait Parser: sealed::Sealed {
    fn parse(&self, input: &str) -> Result<String, String>;
}

pub struct JsonParser;

impl sealed::Sealed for JsonParser {}

impl Parser for JsonParser {
    fn parse(&self, input: &str) -> Result<String, String> {
        Ok(format!("json: {input}"))
    }
}

External crates can use Parser, but they cannot implement it because they cannot name or implement sealed::Sealed.

What this buys you

You can safely add new methods to Parser later, because you control every implementor. You can also assume that all implementations obey the invariants established by your crate.


A practical example: a database backend API

Suppose you are writing a storage library that supports a few built-in backends. You want users to select a backend and call a common interface, but you do not want arbitrary third-party implementations.

mod sealed {
    pub trait Sealed {}
}

pub trait Backend: sealed::Sealed {
    fn name(&self) -> &'static str;
    fn connect(&self) -> Result<(), BackendError>;
}

#[derive(Debug)]
pub struct BackendError;

pub struct Postgres;
pub struct Sqlite;

impl sealed::Sealed for Postgres {}
impl sealed::Sealed for Sqlite {}

impl Backend for Postgres {
    fn name(&self) -> &'static str {
        "postgres"
    }

    fn connect(&self) -> Result<(), BackendError> {
        Ok(())
    }
}

impl Backend for Sqlite {
    fn name(&self) -> &'static str {
        "sqlite"
    }

    fn connect(&self) -> Result<(), BackendError> {
        Ok(())
    }
}

Now your public API can accept impl Backend:

pub fn initialize<B: Backend>(backend: B) -> Result<(), BackendError> {
    backend.connect()
}

This gives you a clean interface while preserving control over supported backends.


When sealing is the right choice

Sealed traits are not a default solution for every trait. They are best used when the trait is part of a controlled abstraction rather than an open extension point.

Good fits

Use caseWhy sealing helps
Built-in enum-like behaviorKeeps the set of valid implementations closed
Internal protocol layersPrevents unsupported wire formats or state transitions
Library backendsEnsures all implementations satisfy hidden invariants
Public helper traitsLets you add methods later without breaking external impls
Marker traits with semanticsAvoids accidental misuse by downstream crates

Poor fits

Use caseWhy sealing is a bad fit
Plugin ecosystemsUsers need to implement the trait
Framework extension pointsThird-party integration is the goal
Public abstraction layers meant for compositionSealing blocks legitimate customization

If your trait is intended as a stable extension surface, do not seal it. Sealing is about control, not convenience.


Sealing and API evolution

One of the strongest reasons to seal a trait is future compatibility.

Imagine you publish this trait:

pub trait Serializer {
    fn serialize(&self, value: &str) -> String;
}

Later, you want to add:

fn format_hint(&self) -> Option<&'static str>;

If external crates implement Serializer, your new method may require a default implementation, or it may break existing implementations. With a sealed trait, you can add methods more freely because you own every impl.

Design benefit

Sealing lets you make stronger assumptions about:

  • method semantics
  • return value constraints
  • object lifecycle
  • internal caching strategies
  • compatibility with future features

This is especially valuable in libraries that need long-term stability.


Combining sealing with enums and exhaustive matching

Sealed traits often complement enums. A common pattern is to expose a trait for behavior while keeping the actual implementors limited to a known set of types.

For example, a parser API might support only a few formats:

  • JsonParser
  • YamlParser
  • TomlParser

You can use a sealed trait to ensure those are the only valid implementations while still allowing polymorphic code:

pub fn run_parser<P: Parser>(parser: P, input: &str) -> Result<String, String> {
    parser.parse(input)
}

This gives you generic dispatch without opening the trait to arbitrary downstream code.


Sealed traits with associated types

Sealing is especially useful when a trait has associated types that encode internal assumptions.

mod sealed {
    pub trait Sealed {}
}

pub trait Storage: sealed::Sealed {
    type Key;
    type Value;

    fn get(&self, key: &Self::Key) -> Option<Self::Value>;
}

If external code could implement Storage, it might choose incompatible Key or Value types that violate your intended model. Sealing ensures that only your crate defines the allowed combinations.

This is a common technique in libraries that model:

  • typed caches
  • message codecs
  • database rows
  • platform-specific backends

A common alternative: private fields instead of sealed traits

Sometimes developers use private fields in a struct to prevent external construction or extension. That works for types, but not for trait implementations.

Here is the difference:

  • private fields control how a type is instantiated
  • sealed traits control who can implement behavior

They solve different problems. If your goal is to preserve trait invariants, sealing is the right tool.


Best practices for implementing sealed traits

Keep the sealing module private

The sealed module should not be part of your public API surface. It should be hidden and minimal.

mod sealed {
    pub trait Sealed {}
}

Do not add extra public items there unless they are truly internal implementation details.

Make the public trait inherit from the sealed trait

This is the key enforcement mechanism:

pub trait MyTrait: sealed::Sealed {
    // methods
}

If you forget this inheritance, the pattern does not work.

Implement the sealed trait only for supported types

Every type that implements the public trait must also implement the sealed trait. Keep those impls close together so the relationship is obvious.

Document the intent

Even though the compiler enforces the restriction, your API docs should explain why the trait is sealed. This helps users understand that the limitation is intentional, not accidental.

A short note like “This trait is sealed and cannot be implemented outside this crate” is usually enough.


Sealed traits and blanket implementations

Be careful with blanket impls. They can accidentally widen your API in ways that undermine the purpose of sealing.

For example:

impl<T> sealed::Sealed for T {}

This would effectively make every type implement the sealed trait, defeating the pattern entirely. Avoid blanket impls unless they are tightly constrained and still preserve the intended restriction.

A safer approach is to implement the sealed trait only for concrete types you control.


Comparison: sealed vs open traits

Trait styleExternal impls allowedAPI evolution flexibilityBest for
Open traitYesLowerPlugins, user extensions, framework hooks
Sealed traitNoHigherControlled abstractions, internal invariants, stable backends

This tradeoff is the heart of the decision. If you need openness, keep the trait open. If you need control, seal it.


Testing and maintenance considerations

Sealed traits can simplify testing because you know the exact set of implementors. That makes it easier to write exhaustive tests for behavior and edge cases.

However, there is a maintenance cost: if you later decide to open the trait, that is a breaking API change in the opposite direction. Users may have built assumptions around the trait being closed. Treat the decision as part of your public contract.

Practical guidance

  • seal traits that represent internal policy or protocol rules
  • leave extension traits open when user customization is the primary goal
  • prefer sealed traits for “supported implementations only” APIs
  • avoid sealing just to discourage usage; use documentation for that

A real-world design checklist

Before sealing a trait, ask:

  1. Will third-party implementations be genuinely useful?
  2. Do I need freedom to add methods later?
  3. Are there hidden invariants that external code cannot safely uphold?
  4. Is the trait part of a closed set of supported behaviors?
  5. Would a plugin model be better served by an open trait?

If the answers lean toward control and stability, sealing is likely appropriate.


Conclusion

Sealed traits are a small pattern with a large impact on API design. They let you publish behavior without surrendering control over who can implement it. That makes them ideal for libraries that need stable abstractions, controlled backends, or room to evolve safely.

Used well, sealing helps you design Rust APIs that are both ergonomic for consumers and maintainable for library authors. It is one of the most practical tools for balancing extensibility with long-term correctness.

Learn more with useful resources