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

Structuring APIs for Evolution

Design interfaces that accommodate future growth without breaking consumers.

In this lesson, you will learn how to structure your Rust APIs so that you can add new functionality in the future without breaking the code your users have already written.

Anticipating Change

When you publish a library or module, your public interface becomes a contract. If you change that contract, you break your users' code. To build for evolution, you must separate the data you expose from the logic you provide.

The "Opaque Struct" Pattern

A common mistake is making all fields in your structs pub. If you ever need to add a field, change a type, or add validation logic to those fields later, you are forced to break your API. Instead, use private fields and expose data through methods.

Consider this API design:

pub struct Config {
    // Making this public allows users to set it directly.
    // If you later need to ensure `timeout` is never 0, you can't!
    pub timeout: u64, 
}

Instead, design it like this:

pub struct Config {
    timeout: u64,
}

impl Config {
    pub fn new(timeout: u64) -> Self {
        Self { timeout }
    }

    pub fn timeout(&self) -> u64 {
        self.timeout
    }
}

Why This Evolves

By keeping timeout private, you control the access point. If you later decide you want to support both seconds and milliseconds, or enforce a minimum timeout value, you can update your new method and internal logic without changing the signature of how the user accesses the data via timeout().

Furthermore, if you need to add new configuration options, you can add them to the struct without breaking existing initialization patterns if you provide a Default implementation or use a Builder pattern (which you’ve already explored in our Factory lessons).

By hiding your implementation details behind methods and keeping your data structures private, you reserve the right to change how you store and process information, while the interface remains stable for your consumers. You are effectively decoupling the user's dependency on your internal representation.

Key Takeaway: Hide your fields behind methods to ensure your internal logic can evolve while keeping your public API contract stable.