04
Product
16
Backend
09
Auth
12
iOS
07
Infra
02
Real-Time

Use cfg!() runtime checks over #[cfg] conditionals

ADR-0007 ACCEPTED · 2025-07-11
Use cfg!() runtime checks over #[cfg] compile-time conditionals

Context

Rust provides two ways to handle conditional compilation based on build configuration:

  1. Compile-time conditionals (#[cfg]): Code is included/excluded at compile time
  2. Runtime checks (cfg!()): All code is compiled, conditions evaluated at runtime

We need to decide which approach to use for debug vs. release behavior differences (GraphQL complexity limits, introspection, playground access, etc.).

Compile-time issues we encountered:

  • Debug and release builds can have different compilation errors
  • cargo check vs cargo check --release may succeed/fail differently
  • Conditional compilation makes code harder to maintain and reason about

Decision

Use cfg!() runtime checks instead of #[cfg] compile-time conditionals for configuration-dependent behavior.

Examples:

// Preferred: Runtime check
.limit_complexity(if cfg!(debug_assertions) { 1000 } else { 100 })

// Avoid: Compile-time conditional
#[cfg(debug_assertions)]
.limit_complexity(1000)
#[cfg(not(debug_assertions))]
.limit_complexity(100)

Exception: Only use #[cfg] for test-only code that shouldn't exist in production builds:

#[cfg(test)]
mod tests { ... }

Consequences

What Becomes Easier

  • Single code path - both debug and release builds compile the same code
  • Consistent compilation - cargo check and cargo check --release behave identically
  • Easier maintenance - no duplicate code paths for different configurations
  • Better IDE support - code analysis tools see all code paths

What Becomes More Difficult

  • Slight runtime overhead - configuration checked at runtime vs. compile time
  • Dead code in binaries - unused branches included in final executable
  • Less aggressive optimization - compiler can't eliminate dead code paths completely