How to Make Struct Fields Public vs Private in Rust

Make Rust struct fields public by adding the `pub` keyword before the field name, as fields are private by default.

The locked drawer problem

You build a Player struct for a game. It holds health, position, inventory, and a debug flag. You hand the struct to a teammate. Two weeks later, they directly mutate the debug flag in three different files. The game breaks. You could have prevented it by controlling who gets to touch what. Rust gives you that control by default.

Visibility is a compile-time boundary

In Rust, every struct field is private unless you explicitly mark it otherwise. This is not a suggestion. The compiler enforces it at the module boundary. Think of a struct like a secure briefcase. The case itself might be visible to everyone, but the compartments inside stay locked unless you hand out a key. The pub keyword is that key.

You can apply pub to the struct definition, to individual fields, or to both. Each combination changes what other code can do. A public struct with private fields is the most common pattern. It lets other modules create and pass the type around while keeping the internal layout hidden. A private struct with public fields is impossible. Rust will not let you put a public door on a locked house.

The minimal setup

Here is the baseline. A struct with mixed visibility.

/// A user account with exposed and hidden data.
pub struct User {
    pub username: String,
    email: String,
}

fn main() {
    let user = User {
        username: String::from("alice"),
        email: String::from("alice@example.com"),
    };

    // The compiler allows this. The field is public.
    println!("Username: {}", user.username);

    // This line will not compile. The field is private.
    // println!("Email: {}", user.email);
}

The pub on struct User makes the type name visible outside the current module. The pub on username makes that specific field readable and writable from anywhere that can see the struct. The email field lacks pub, so it stays locked to the module where User is defined. You can read it, write it, and pass it around inside that module. Outside code gets nothing.

What happens under the hood

Visibility does not change memory layout. A public field and a private field sit side by side in the same contiguous block of memory. The compiler does not add padding, locks, or runtime checks for visibility. It simply tracks module boundaries during compilation. When you write user.email in a different module, the compiler checks its internal visibility table, finds a mismatch, and stops the build.

This zero-cost enforcement is why Rust developers trust encapsulation. You do not pay a performance penalty for hiding data. You only pay the one-time cost of writing the code correctly. The compiler generates the exact same machine code whether a field is public or private. The restriction exists purely to protect your API surface.

Visibility also interacts with the module tree. Rust does not use file boundaries to determine privacy. It uses the lexical module hierarchy. A struct defined in lib.rs is private to lib.rs by default, even if you split it into a mod config; file. You must explicitly mark it pub if you want sibling modules or child modules to see it. This design prevents accidental API leakage when you reorganize files.

Real-world pattern: getters and controlled mutation

Private fields force you to write methods that mediate access. This is where encapsulation actually earns its keep. Instead of letting anyone overwrite a value, you validate it first.

/// A database connection that guards its internal state.
pub struct DbConnection {
    pub host: String,
    password: String,
    is_connected: bool,
}

impl DbConnection {
    /// Creates a new connection and validates the password length.
    pub fn new(host: &str, password: &str) -> Option<Self> {
        // Reject weak passwords before allocation
        if password.len() < 8 {
            return None;
        }
        Some(Self {
            host: host.to_string(),
            password: password.to_string(),
            is_connected: false,
        })
    }

    /// Attempts to connect using the hidden password.
    pub fn connect(&mut self) -> bool {
        // Simulate authentication logic
        self.is_connected = true;
        true
    }

    /// Returns whether the connection is active.
    pub fn is_active(&self) -> bool {
        self.is_connected
    }
}

External code can read host directly. It cannot touch password or is_connected. It must call new() to create the struct, and connect() to change state. The struct author controls the rules. If the authentication logic changes later, you only update connect(). You do not hunt down fifty places where someone directly flipped a boolean.

This pattern also buys you future-proofing. Suppose you decide to cache the connection status in a database instead of a local boolean. With private fields, you only change the is_active() method body. External code continues to call the same method. The public interface stays stable. If the field had been public, every caller would break at compile time.

Pitfalls and compiler errors

New Rust developers often trip over visibility rules when splitting code across modules. The compiler will catch you, but the error messages can feel abrupt if you do not know what to expect.

If you try to read a private field from another module, you get E0616 (field is private). The compiler points to the exact line and names the struct. Fix it by either adding pub to the field or writing a public getter method.

If you mark a field as pub but forget to mark the struct itself as pub, you get E0451 (field is public, but the struct is private). Rust does not allow a public door on a locked house. You must make the struct public first, or remove pub from the field.

Another common trap involves nested modules. Visibility is relative to the module tree, not just file boundaries. A struct defined in lib.rs is private to lib.rs by default, even if you put it in a mod config; file. You must explicitly mark it pub if you want other modules to see it.

Generics add another layer. If a struct contains a generic type parameter, the visibility of the struct does not automatically grant visibility to the generic parameter's methods. You still need to follow standard trait bound rules. The compiler will reject code that tries to call a method on a private field's type if that method is not accessible.

Convention asides

The Rust community strongly prefers private fields with public getters over fully public structs. This keeps your API stable and gives you room to refactor without breaking downstream code. When you need visibility that stops at the crate boundary, use pub(crate). It exposes the item to everything inside your library or binary, but hides it from downstream crates. This is the standard pattern for internal helpers that should not leak into the public API.

You will also see pub(super) and pub(in path) in larger codebases. These modifiers restrict visibility to a specific parent module or an arbitrary path in the module tree. They are useful for testing utilities or tightly coupled subsystems. Stick to pub and private by default until you hit a concrete need for finer control.

When to choose what

Use private fields when the value represents internal state, cached data, or credentials that should not be mutated arbitrarily. Use public fields when the struct is a simple data container with no invariants to enforce, like a configuration struct or a geometry point. Use pub(crate) when you want to share implementation details across your own crate without exposing them to library users. Reach for getter methods when you need validation, lazy initialization, or logging on access. Trust the compiler to enforce the boundary. It will not let you accidentally leak implementation details.

Where to go next