Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

global-state

Level: warn · Article: Dependencies

Invisible dependencies are the cockroaches of software architecture: everywhere, impossible to count, surviving every refactor.

What it checks

A static with interior mutability (Mutex, RwLock, OnceLock, OnceCell, LazyLock, Cell, RefCell, atomics), static mut, or a lazy_static! block. Statics whose name contains LOG are exempt by default: a logger is infrastructure nobody swaps in tests.

Don’t

#![allow(unused)]
fn main() {
static DATABASE: OnceLock<Database> = OnceLock::new();

impl User {
    async fn load(id: &UserId) -> Result<User, LoadError> {
        let db = DATABASE.get().ok_or(LoadError::NotConnected)?; // hidden dependency
        db.query(id).await
    }
}
}

Zero constructor parameters. Looks simple. Until two tests run in parallel against the same global, or you need to point it at another database.

Do

struct Users {
    db: Database,
}

impl Users {
    async fn load(&self, id: &UserId) -> Result<User, LoadError> {
        self.db.query(id).await
    }
}

#[tokio::main]
async fn main() -> Result<(), StartupError> {
    // every dependency constructed in one place, then handed down
    let config = Config::from_env()?;
    let db = Database::connect(&config.db_url).await?;
    let users = Users { db };
    Api::new(users).serve().await
}

Exactly as complex as it actually is, and checked at compile time.

Options

[global-state]
allowed-names = ["LOG"]   # substring match, case-insensitive

Silence it

#![allow(unused)]
fn main() {
// rabot: allow(global-state) compiled-once regex; a pure value, never swapped
static EMAIL: LazyLock<Regex> = LazyLock::new(|| Regex::new(..).unwrap());
}