Articles
Cargo Feature Interview Questions: Unification, Optional Dependencies, and Workspace Builds
Practice Cargo feature interviews with a Rust workspace: trace feature unification, optional dependencies, cfg gates, and minimal consumer build failures.
Cargo feature interview questions often start with a confusing observation: the workspace builds, yet a downstream consumer cannot compile the same library. The explanation is usually in the selected dependency graph, the features requested along its edges, and which code is actually removed before type checking.
The central rule is to reason about the resolved feature set for a package in this build, rather than the options written by one caller. A successful broad build can hide a missing dependency in a smaller configuration.
This guide separates official Cargo and Rust rules from an original, executed local exercise and engineering recommendations. It contains no candidate reports or claims about an employer's screening format.
Why Can the Workspace Pass While One Consumer Fails?
Use this original four-package workspace: codec, an optional local dependency called json_support, and two binaries, slim and rich. The names describe a toy demonstration, not a production JSON implementation.
# Workspace root Cargo.toml
[workspace]
members = ["codec", "json_support", "slim", "rich"]
resolver = "2"
The codec's manifest contains:
[package]
name = "codec"
version = "0.1.0"
edition = "2021"
[features]
default = ["std"]
std = []
json = ["dep:json_support"]
[dependencies]
json_support = { path = "../json_support", optional = true }
The std feature is just a marker in this fixture. There is no #![no_std] declaration, so disabling it does not demonstrate a genuinely standard-library-free crate.
In slim, the dependency is codec = { path = "../codec", default-features = false }. In rich, it is codec = { path = "../codec", features = ["json"] }. Both binaries call codec::render(). The local helper's entire implementation is pub fn label() -> &'static str { "json" }.
Now put this deliberately broken function in the codec:
pub fn render() -> &'static str {
if cfg!(feature = "json") {
json_support::label()
} else {
"plain"
}
}
Official fact: cfg! evaluates a configuration predicate into a Boolean; conditional compilation with #[cfg] determines whether the annotated code is included. A false cfg! condition does not remove the other branch from the source that must be checked. Rust conditional-compilation reference
Observed local result: with Cargo 1.98.1 and local path dependencies, cargo check --workspace --offline passed. cargo check -p slim --offline failed with an unresolved json_support dependency. The wide build selected the rich consumer too, which requested the missing capability.
This is a feature-dependent compilation failure, not evidence that Cargo reused the wrong artifact. Deleting the target directory does not repair the source contract.
How Does Feature Unification Explain the Difference?
Official fact: feature requests for a shared package are combined in the relevant dependency graph. Resolver 2 still unifies ordinary dependencies, while separating certain build/dev/target situations. It does not give every consumer a private feature configuration. Cargo dependency resolution
For the toy workspace, trace the edges explicitly:
| Selected build | Requests reaching codec | Consequence for the broken function |
|---|---|---|
-p slim | No default features and no json | Optional helper absent; its reference fails checking |
-p rich | Default std, plus json | Helper present; function compiles |
--workspace | Union of slim and rich requests | Helper present; slim's minimal configuration is not independently proved |
The important interview move is to name the selected roots before calculating the union. Two separate command invocations do not keep each other's enabled features as persistent settings.
Then ask whether the dependency is actually the same package identity. A package name alone can be misleading when different versions or sources occur. Investigate the resolved graph before claiming that two requests must merge.
The original design mistake is treating a feature as if it belonged exclusively to the caller who enabled it. For a reusable library, additional capabilities should generally coexist. If enabling a second feature changes a fundamental public type or removes an existing method, consumers can interfere with one another through the union.
What Is the Smallest Correct Repair?
Gate the definitions themselves:
#[cfg(feature = "json")]
pub fn render() -> &'static str {
json_support::label()
}
#[cfg(not(feature = "json"))]
pub fn render() -> &'static str {
"plain"
}
Observed local result: after this repair, the slim check and the workspace check both passed. Running slim printed plain; running rich separately printed json. The codec also passed checks with no default features and with all features. These results establish this small fixture's configurations, not every possible Cargo resolver or target combination.
Notice a product-contract issue remains even in compiling code. In a joint build, slim can receive a codec with JSON enabled. If slim requires “plain output” as a behavioral guarantee, implicit feature-based selection is the wrong interface. Provide explicit runtime methods or a configuration value so the caller chooses the format.
This separates two repairs: conditional compilation fixes the absent dependency, while explicit behavior selection fixes caller expectations. In an interview, stopping at “it compiles now” misses the second boundary.
Does default-features = false Turn Defaults Off Everywhere?
Official fact: a dependency declaration can opt out of default features, but another dependency path can still request them. Optional dependencies can be exposed through named features; using dep: suppresses the dependency's implicit same-name feature. Cargo features reference
In the fixture, slim opts out on its own edge. Rich does not. Therefore a build containing both can include codec's std marker. There is no “false wins” rule that subtracts rich's request.
A useful response is: “I will inspect every path that enables this package's defaults, then test a consumer whose entire graph supports the desired minimal configuration.” Editing just one manifest can leave another path untouched.
Also distinguish the selected package's command-line options from its dependencies' manifest declarations. cargo check -p slim --no-default-features does not rewrite rich's manifest or every dependency's defaults. Be precise about which package the flag targets.
For a real no-std library, the proof should include the target and code paths that forbid standard-library use. Our toy marker cannot supply that proof. This limitation is useful practice: do not infer environmental guarantees from feature names.
How Do Optional Dependencies and Forwarding Differ?
Suppose a library has an optional backend dependency with a fast feature. These are three different intentions:
| Feature entry | Intended relationship | Review question |
|---|---|---|
backend = ["dep:backend"] | Expose a chosen public feature that enables the optional dependency | Is that name part of the supported public interface? |
fast = ["backend/fast"] | Enable the backend and its fast feature | Should fast require the backend to exist? |
fast = ["backend?/fast"] | Forward fast only if something else enables the backend | Is fast allowed to do nothing without the backend? |
The ? forwarding syntax is an official Cargo feature mechanism; it is not a missing-dependency error suppressor. Cargo features reference
This table is a separate reasoning exercise, not an executed part of the four-package fixture. Ask the interviewer which user-facing behavior is intended before choosing one entry. A conditional forwarding feature can be correct for an optional integration, but surprising if users expect it to activate the integration automatically.
Official fact: Cargo's real-world examples show libraries re-exporting dependency features so callers can request capabilities without directly depending on implementation packages. Cargo feature examples
Engineering inference: keeping the internal package name out of the public feature vocabulary can make later dependency replacement easier. It does not eliminate the compatibility obligation for the public capability itself. Document the user promise, not merely the TOML wiring.
What Changes With Workspaces and Resolver Versions?
Official fact: the resolver selection is workspace-wide. A virtual workspace needs an explicit resolver because it has no root package edition from which to infer the choice. Workspace dependency inheritance also has its own manifest rules. Cargo workspaces reference
This fixture deliberately sets resolver 2 to make its assumptions visible. Do not change the resolver blindly as a compilation fix. First identify the dependency kind, selected packages, target, and command that produces the failure.
Resolver 2 can distinguish build-dependency feature use from normal-dependency use, avoid activation from inactive target dependencies, and limit dev-dependency activation to builds that need them. Those separations can cause the same dependency to be built in multiple configurations. Cargo dependency resolution
That is different from our two ordinary consumers sharing codec. The fixture remains unified in the workspace build. A confident answer explains why the relevant exception does or does not apply instead of repeating “resolver 2 fixes features.”
When inherited dependencies are involved, inspect the workspace declaration as well as the member manifest. A member's small-looking dependency line may inherit defaults or feature requests from the root. Treat the complete declaration as evidence.
Which Commands Produce Useful Evidence?
Official fact: cargo tree can show feature edges, invert the graph around a dependency, and display enabled features in package formatting. Its view is a diagnostic aid, not a replacement for compiling the intended configuration. Cargo tree documentation
For this workspace, begin with:
cargo tree -p slim -e features -i codec --offline
cargo check -p slim --offline
cargo check --workspace --offline
cargo check -p codec --no-default-features --offline
cargo check -p codec --all-features --offline
Keep the selection consistent between the tree inspection and the failing check. An unqualified tree for a broader graph can explain the wrong build. Use package/version disambiguation if the dependency name is not unique.
For a reusable library, add a small external consumer when packaging or publication matters. Official fact: local path dependencies and registry version requirements serve different dependency-resolution roles. Passing a path-based fixture does not demonstrate that a published package includes every required file or valid registry dependency. Cargo dependency declarations
Choose the test matrix from the supported contract: minimum features, defaults, each important capability, meaningful combinations, relevant targets, and downstream use. An all-features check is useful but can conceal missing gates, exactly as the original workspace did. Avoid an exponential matrix when a smaller set of interaction cases answers the actual risks.
How Would You Explain the Failure Without Guessing?
Try this original interview follow-up: “The CI job passes, but a customer's build fails. What evidence do you request?” Ask for the exact command, selected package, target triple, toolchain version, lockfile, and complete dependency declarations. Reduce the failing graph before changing features.
For our fixture, a useful explanation is: “The customer builds slim alone. Its codec edge enables no features. The source still references json_support because cfg! leaves both branches for checking. Workspace CI includes rich, whose json request makes that dependency available. I will gate the definitions, then validate both selections.”
If the compiler instead reports two definitions of the same function, the diagnosis changes: two enabled features may each include an implementation. The previous repair is not automatically applicable. Draw the enabled predicates and identify whether they overlap, then choose a documented precedence or an additive interface. A feature-debugging answer earns its clarity from the specific compiler error and graph, rather than a list of Cargo flags.