Skip to content

The Rust Engineering Handbook / Chapter 20

Coherence, Orphan Rules, and Extension Patterns

Resolve forbidden and conflicting implementations with explicit ownership, wrappers, extension traits, sealing, and conversion boundaries.

The method is easy; owning its meaning is not

A settlement service receives WireAmount values from one dependency and must write them through a reporting API owned by another. The integration crate wants ordinary formatting at the boundary:

use std::fmt::{self, Display, Formatter};
use wire_types::WireAmount;

impl Display for WireAmount {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{} cents", self.cents())
    }
}

The body is harmless. The implementation is not. Display belongs to the standard library, WireAmount belongs to wire_types, and this integration crate owns neither side of the relationship. Rust rejects it with E0117.

Imagine that the rule were allowed. A second integration crate could quite reasonably render the same amount as USD 12.34. A binary that acquired both dependencies would then contain two meanings for WireAmount: Display. Dependency order, import order, and proximity are poor ways to choose the behavior of generic code. Adding an unrelated crate could change or invalidate a call that had already been compiled and reviewed.

Coherence prevents that ambiguity by requiring at most one applicable trait implementation. The orphan rules assign which crate may establish a trait/type relationship; overlap checking ensures that the implementations it can write do not compete. The rejection is therefore useful design evidence: this crate may perform the formatting, but it may not claim that the formatting is the one global meaning of two foreign definitions.

Locality grants responsibility

For the ordinary, non-generic case, a crate can implement its local trait for a foreign type or a foreign trait for its local type. It controls at least one side, so other crates cannot independently create the same pairing. A foreign trait on a foreign type has no such owner here.

The full orphan rules also account for type parameters, fundamental types, and the order in which parameters are covered by local types. The Reference and the compiler diagnostic are the authority for those edges. “One side is local” is a good first model, not a substitute for the formal rule.

Locality is nominal. Moving the rejected implementation into a local module does nothing. Re-exporting WireAmount changes its path, not its identity. A type alias gives the same type another spelling:

type SettlementAmount = WireAmount; // still foreign

None of those operations accepts responsibility for a new type or contract. The repair must say what this integration actually owns.

Give settlement formatting a type of its own

Here the rendering is part of the settlement service’s reporting policy. A newtype gives that policy a local nominal home:

use std::fmt::{self, Display, Formatter};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SettlementAmount(WireAmount);

impl SettlementAmount {
    pub const fn from_wire(amount: WireAmount) -> Self {
        Self(amount)
    }

    pub const fn cents(self) -> u64 {
        self.0.cents()
    }
}

impl Display for SettlementAmount {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let cents = self.0.cents();
        write!(f, "USD {}.{:02}", cents / 100, cents % 100)
    }
}

SettlementAmount is not camouflage for defeating the compiler. It states that this service owns a monetary representation with a particular display policy. Conversion at the boundary is visible, and future validation or redaction rules have somewhere to live.

That ownership has a cost. Callers must construct the wrapper, decide when to borrow or unwrap it, and explicitly forward any inner behavior that belongs on the public surface. Those costs are often desirable: they expose the exact places where foreign data acquires local meaning.

Do not add Deref<Target = WireAmount> merely to recover every inner method. Deref participates in method lookup and coercion; using it as inheritance can erase the boundary the newtype was introduced to create. Likewise, #[repr(transparent)] is relevant only when its documented representation guarantees are required. It does not make the coherence argument stronger.

An implementation ownership matrix marks local trait plus any type and foreign trait plus local type as permitted cases, while foreign trait plus foreign type is rejected. A decision path routes representation identity to a newtype, method convenience to an extension trait, closed implementers to a sealed trait, and explicit adaptation to a wrapper or conversion.
Coherence assigns the right to make a global implementation. When neither the trait nor implementing type is local, make the integration boundary explicit.

Add convenience without inventing identity

The same service also calculates a fee on a WireAmount. That operation is useful at several call sites, but it does not make the value a new domain type. A local extension trait can add the vocabulary without claiming a foreign standard-trait relationship:

pub trait WireAmountExt {
    fn checked_fee(&self, basis_points: u32) -> Option<u64>;
}

impl WireAmountExt for WireAmount {
    fn checked_fee(&self, basis_points: u32) -> Option<u64> {
        self.cents()
            .checked_mul(u64::from(basis_points))?
            .checked_div(10_000)
    }
}

The trait is local, so this crate owns every implementation of it. Callers opt in by importing WireAmountExt. That makes extension methods well suited to a small, cohesive vocabulary used repeatedly with method syntax.

The opt-in is also a limitation. IDE discovery may be weaker until the trait is in scope, and another imported trait may define the same method name. Fully qualified syntax can disambiguate the call, but an ordinary function such as checked_wire_fee(&amount, bps) is often clearer for one isolated operation. Choose an extension trait because its vocabulary forms a useful local capability, not because a method looks more fluent than a function.

The two repairs now express different ownership. SettlementAmount owns a semantic identity and its standard formatting policy. WireAmountExt owns convenient local operations on an unchanged foreign value. Neither is a universal workaround for E0117.

Overlap spends space even when every definition is local

Owning a trait permits implementations; it does not permit ambiguity. Suppose the reporting crate introduces ReportLabel and tries to provide both a fallback and a special label:

trait ReportLabel {
    fn label(&self) -> &'static str;
}

impl<T> ReportLabel for T {
    fn label(&self) -> &'static str { "value" }
}

impl ReportLabel for SettlementAmount {
    fn label(&self) -> &'static str { "settlement amount" }
}

The blanket implementation already includes SettlementAmount. Rust reports E0119 rather than choosing the more specific body. Source order cannot make trait selection coherent, and stable Rust does not use specialization to rank these implementations.

The broad impl consumed the whole implementation space for ReportLabel. Possible repairs make selection explicit: narrow the blanket bound so the sets are provably disjoint, put special behavior on a wrapper, use separate traits, or replace the trait with a function that performs an explicit match. Each repair answers who selects the behavior. None merely persuades the compiler to prefer one overlapping answer.

This is the danger in convenient blanket implementations. An impl for every T satisfying a bound also covers types that have not been written yet. It can prevent this crate from specializing later and can collide with downstream implementations after either library evolves.

Conversions are implementations too

It is tempting to make the newtype accept anything convertible to WireAmount:

// Broad, and probably broader than the domain contract.
impl<T: Into<WireAmount>> From<T> for SettlementAmount { /* ... */ }

This impl claims every present and future T: Into<WireAmount> relationship. It also participates in the standard library’s conversion blanket impls. A later dependency release or a new local conversion can create overlap that was not visible when the convenience impl was published.

Prefer From<WireAmount> when conversion is infallible and semantically unambiguous. Use a carefully scoped TryFrom<WireAmount> when the wrapper enforces representability. Use a named constructor when conversion depends on currency, configuration, rounding, or another policy callers should see. TryFrom communicates failure, but it still occupies coherence space; its scope deserves the same review as any other trait implementation.

Seal only a deliberately closed capability

Suppose the reporting library exposes storage modes and must exhaustively know which ones are durable. Allowing arbitrary downstream implementations would make that promise impossible. A private supertrait can close the set:

mod private {
    pub trait Sealed {}
}

pub trait StorageMode: private::Sealed {
    const DURABLE: bool;
}

pub struct Memory;
pub struct Durable;

impl private::Sealed for Memory {}
impl private::Sealed for Durable {}
impl StorageMode for Memory { const DURABLE: bool = false; }
impl StorageMode for Durable { const DURABLE: bool = true; }

Other crates can name StorageMode in bounds but cannot satisfy its private prerequisite for their own types. The library gains a closed capability vocabulary and more freedom to add required items. Users lose the ability to extend that vocabulary.

That trade is an API promise, not a clever coherence technique. Seal a trait when consumers are meant to select among library-owned cases. Leave it implementable when third-party implementations are the product’s extension boundary. A trait advertised for plugins and secretly sealed contradicts itself.

Compatibility includes absent downstream code

Adding an impl can break code without removing any item. A downstream crate may already have a legal implementation involving its own local trait or type; a new blanket or conversion impl can make the combined program overlap. Public implementations therefore belong in compatibility review alongside functions and types.

Before publishing one, ask which future types it captures, which downstream local wrappers can participate, and whether a named operation would preserve more room. Sealing reduces the set of unknown implementers, but only by making the closed extension contract explicit. Chapter 44 develops the wider SemVer policy; the immediate lesson is that additive source text need not be additive in a dependency graph.

Negative impls and specialization-related features have narrow, unstable, or language-defined roles depending on the trait and feature. They are not a stable production escape from overlap. Prefer positive local types, disjoint traits, adapters, or explicit strategies whose selection can be read in the program.

Resolve the integration without hiding the owner

For the settlement service, retain fixtures that demonstrate E0117 for the foreign Display implementation and E0119 for the blanket-plus-specific label implementations. Then complete three legal paths:

  1. Make SettlementAmount own the reporting identity and implement Display only for that type.
  2. Add checked fee calculation through WireAmountExt, then compare it with a free function at call sites where the trait import is not otherwise useful.
  3. Write a named adapter from WireAmount into the reporting API when neither a lasting identity nor reusable method vocabulary is needed.

Now change the requirements. Support two currencies whose formatting needs configuration, and add a downstream crate that already wraps WireAmount. Decide which conversions remain honest and which should become named constructors. Sketch any blanket implementation you are considering and mark the future types it captures before writing it. Do not use a type alias, re-export, Deref, specialization, or negative reasoning to conceal the ownership decision.

Review the implementation space

  • Which crate owns the trait, the implementing type, or both?
  • Does the implementation establish a global semantic relationship or only provide local adaptation?
  • Would a newtype express identity, an extension trait express vocabulary, or a function express a one-off operation more honestly?
  • What current and future types does each generic implementation cover?
  • Can two legal dependency releases make previously separate impls overlap?
  • Is conversion infallible and context-free enough to deserve From?
  • Is a sealed implementation set genuinely part of the public contract?
  • Does the stable design make behavior selection visible without relying on specialization?

Coherence answers who may attach a trait’s meaning to a type. Once that relationship is legal and unambiguous, the next choice is when a caller learns the concrete implementer. Chapter 21 follows that choice through static dispatch, closed enums, and trait objects.

Sources and version notes