How the Facade+Stub Pattern in OpenHuman Prevents Compile-Time Drift in Optional Features

The facade+stub pattern in OpenHuman uses unconditional facade modules that re-export either a feature-gated implementation or a parallel stub module, ensuring the compiler validates API consistency across all build configurations and preventing silent symbol drift.

The tinyhumansai/openhuman repository manages complex optional capabilities—voice processing, Web3 integrations, MCP tool registries—using a disciplined facade+stub architecture that eliminates compile-time drift. This Rust design pattern guarantees that public APIs remain type-stable regardless of whether specific Cargo features are enabled, allowing developers to toggle capabilities without conditional compilation clutter in caller code.

The Compile-Time Drift Problem

Optional features in Rust typically use #[cfg(feature = "...")] to conditionally compile code. Without careful architecture, disabling a feature removes public symbols entirely, causing later compilation failures when application code references them. This compile-time drift creates a maintenance burden where callers must sprinkle #[cfg] attributes throughout the codebase, and API changes risk breaking disabled builds silently until someone attempts a feature-off compilation.

OpenHuman solves this by decoupling the public facade from the internal implementation, ensuring symbols always exist even when functionality is disabled.

Architecture of the Facade+Stub Pattern

Unconditional Facade Modules

Each top-level domain in OpenHuman defines a facade module that is always compiled regardless of feature flags. For example, src/openhuman/voice/mod.rs serves as the permanent public interface. This module uses conditional compilation attributes to select between the real implementation and a stub, but the facade itself remains intact.

The facade merely re-exports the public symbols that callers need, presenting a consistent import surface:

// Always available, regardless of feature flags
use openhuman::voice::{create_stt_provider, publish_ptt_transcript_committed};

Feature-Gated Implementation

Behind the facade, the actual logic lives in submodules guarded by #[cfg(feature = "<domain>")]. When the feature is enabled, the real implementation module compiles and registers its controllers, RPC schemas, and runtime services. This code handles the full capability lifecycle, from initialization to event handling.

The registration logic also sits behind the same feature gate, meaning disabled builds simply omit the service endpoints without breaking the compilation graph.

The Stub Module as API Guardian

When a feature is disabled, the facade imports a parallel stub module instead, gated by #[cfg(not(feature = "<domain>"))]. Located at paths like src/openhuman/voice/stub.rs, these files implement the exact same public items—structs, traits, and functions—as the real implementation, but with empty bodies or error returns.

For instance, when the voice feature is disabled, calling publish_ptt_transcript_committed executes:

log::debug!("[voice-stub] publish_ptt_transcript_committed ignored (voice disabled)");

Critically, the stub does not register any controllers or services; it only provides the type-level signatures required for compilation. This ensures the core system can compile and link without the actual service implementations present.

Concrete Implementation Across Domains

OpenHuman applies this pattern consistently across multiple optional domains. The following table maps each facade to its corresponding stub implementation:

Domain Facade (Always Compiled) Stub (Disabled Implementation)
Voice src/openhuman/voice/mod.rs src/openhuman/voice/stub.rs
Web3 (Wallet) src/openhuman/web3/wallet/mod.rs src/openhuman/web3/wallet/stub.rs
Web3 (x402) src/openhuman/web3/x402/mod.rs src/openhuman/web3/x402/stub.rs
Skills src/openhuman/skills/mod.rs src/openhuman/skills/stub.rs
MCP (Registry) src/openhuman/mcp/registry/mod.rs src/openhuman/mcp/registry/stub.rs
Runtime Node src/openhuman/runtime/node/mod.rs src/openhuman/runtime/node/stub.rs

Each stub mirrors the public API of its counterpart with no-op or error-return implementations, maintaining type consistency.

Caller Code Without Conditional Compilation

Application code interacts with these domains without #[cfg] checks. The following example compiles whether or not the voice feature is enabled:

use openhuman::voice::{create_stt_provider, publish_ptt_transcript_committed};

let provider = create_stt_provider(&config);
publish_ptt_transcript_committed(&config, "example transcript");

When voice is enabled, create_stt_provider returns a concrete STT provider and the publish call emits a bus event. When disabled, the stub returns a placeholder provider and logs the ignored call. The caller requires no conditional compilation logic.

Optional Runtime Behavior

While the API surface remains stable, you can still execute runtime logic conditional on feature status when necessary:

if cfg!(feature = "voice") {
    // Only runs when the real implementation exists
    start_voice_server(&config).await?;
}

Even without the cfg! guard, the code compiles because the stub supplies start_voice_server as a no-op function.

Enforcement Mechanisms and Guarantees

The facade+stub pattern creates a compile-time contract that prevents API drift. Because the stub must provide identical symbols to the real implementation, any change to the public API—such as adding a new function, changing a signature, or renaming a type—must be reflected in both the feature-enabled implementation and the stub.

If a developer updates the real implementation but forgets to mirror the change in the stub, the compiler emits an error when building with the feature disabled. This immediate feedback guarantees that the API surface never diverges between enabled and disabled builds, eliminating the silent failures characteristic of compile-time drift.

Summary

  • Unconditional facades provide a stable import surface for optional domains like voice, web3, and mcp.
  • Feature-gated implementations contain the real logic and service registration, compiled only when the corresponding Cargo feature is enabled.
  • Stub modules mirror the public API with no-op implementations, ensuring disabled builds always link successfully.
  • Compiler enforcement requires developers to update stubs when changing public APIs, preventing silent drift between feature configurations.
  • Clean caller code eliminates the need for #[cfg] attributes in application logic while maintaining runtime flexibility.

Frequently Asked Questions

What is compile-time drift in the context of Rust feature flags?

Compile-time drift occurs when conditional compilation with #[cfg(feature = "...")] causes public symbols to disappear in certain build configurations. This creates a scenario where code compiles successfully with features enabled but fails later when someone builds with features disabled, as dependent code references symbols that no longer exist. The facade+stub pattern prevents this by ensuring symbols persist across all configurations.

How does the stub module enforce API consistency?

The stub module implements the exact same public functions, structs, and traits as the real implementation. If a developer adds a new public method to the feature-enabled implementation but fails to add it to the stub, the compiler will generate errors when building with the feature disabled. This forces simultaneous updates to both modules, keeping the API surface synchronized by mechanical necessity.

Can application code detect whether an OpenHuman feature is enabled at runtime?

Yes, though the API remains callable regardless. Callers can use cfg!(feature = "...") to check feature status at runtime and branch accordingly, or they can call the facade methods unconditionally and handle the stub's no-op behavior or error returns. The key benefit is that the code always compiles, removing the need for conditional compilation at call sites.

Which domains in OpenHuman currently use the facade+stub pattern?

According to the OpenHuman source code, the pattern is implemented across six primary domains: voice (src/openhuman/voice/), web3/wallet (src/openhuman/web3/wallet/), web3/x402 (src/openhuman/web3/x402/), skills (src/openhuman/skills/), mcp/registry (src/openhuman/mcp/registry/), and runtime/node (src/openhuman/runtime/node/). Each maintains a mod.rs facade and a corresponding stub.rs file for disabled builds.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →