When building software tools, especially CLI applications and developer utilities, how you handle configuration can make the difference between a tool that integrates seamlessly into diverse workflows and one that becomes a source of friction. The most robust and user-friendly tools don’t force users into a single configuration approach - they offer multiple methods that work together harmoniously.
The Three Pillars of Configuration
Effective configuration typically comes in three forms, each serving distinct use cases:
CLI Flags: Immediate and Explicit
Command-line flags offer the most explicit form of configuration. When you run dprs --port 8080 --debug, there’s no ambiguity about what’s happening. CLI flags excel in:
- Ad-hoc usage: Quick one-off commands with specific parameters
- Script automation: Shell scripts where explicit parameters improve readability
- Testing and debugging: Temporarily overriding default behavior
- Documentation: Self-documenting commands that show exactly what’s configured
The immediacy of CLI flags makes them perfect for exploratory work and situations where you need to see exactly what configuration is being applied.
Environment Variables: Context-Aware Configuration
Environment variables bridge the gap between ephemeral CLI flags and persistent config files. They shine in:
- Containerized environments: Docker, Kubernetes, and cloud platforms make environment variables the natural configuration mechanism
- CI/CD pipelines: Build and deployment systems expect environment-based configuration
- Secret management: Sensitive values like API keys and credentials are safer in environment variables than committed config files
- Multi-environment workflows: Different values for development, staging, and production
Modern deployment infrastructure is built around environment variables, making them essential for tools that need to work in contemporary DevOps workflows.
Config Files: Persistent and Comprehensive
Configuration files provide the most comprehensive and maintainable approach for complex setups:
- Version control: Config files can be committed and tracked alongside code
- Complex configurations: Nested structures, arrays, and detailed settings that would be unwieldy as flags
- Team collaboration: Shared configurations that ensure consistent behavior across a team
- Documentation: Config files serve as living documentation of how a tool is configured
Whether YAML, TOML, JSON, or another format, config files offer the structure needed for sophisticated configuration requirements.
Why All Three Matter
The power isn’t in choosing one method - it’s in supporting all three with clear precedence rules. Users should be able to:
- Set sensible defaults in a config file
- Override for an environment using environment variables
- Fine-tune specific invocations with CLI flags
This precedence chain (config file → environment variables → CLI flags) gives users maximum flexibility while maintaining predictable behavior.
Durable Programming’s Commitment to Flexibility
At Durable Programming, we practice what we preach. Our open source projects embody this configuration philosophy:
dprs - Our Docker container management TUI (terminal user interface) demonstrates configuration flexibility in the Rust ecosystem. Built for developers who need to manage Docker containers efficiently, dprs integrates naturally into different development environments by supporting multiple configuration approaches.
abachrome - Our comprehensive Ruby color management library shows how even specialized libraries benefit from flexible configuration. Whether you’re parsing CSS colors in a build pipeline (environment variables), running one-off color conversions (CLI flags), or managing complex color spaces in an application (config files), abachrome adapts to your workflow.
These projects aren’t just tools - they’re examples of thoughtful design that respects how developers actually work.
Implementation Patterns
Supporting multiple configuration methods requires thoughtful design:
Precedence is Critical
Users must understand which configuration takes priority. The typical precedence from lowest to highest is:
- Hardcoded defaults (in code)
- Config file values
- Environment variables
- CLI flags
This order makes intuitive sense: more specific and immediate configuration methods override more general ones.
Discoverability Matters
Each configuration method should be discoverable:
- CLI flags should appear in
--helpoutput - Environment variables should be documented and ideally follow a consistent naming pattern
- Config file locations and formats should be clearly specified
- Error messages should guide users when configuration is missing or invalid
Validation Should Be Consistent
Whether a value comes from a CLI flag, environment variable, or config file, validation should be identical. A port number is invalid if it’s out of range regardless of how it was specified.
Real-World Scenarios
Consider these common scenarios and how multiple configuration methods solve them:
Local Development: A developer uses config files for their standard setup, but occasionally needs to override the port with a CLI flag when testing multiple instances.
CI/CD Pipeline: The same tool reads from a committed config file for most settings, but uses environment variables for secrets and environment-specific values like API endpoints.
Container Deployment: In Kubernetes, environment variables configure the tool for different namespaces and environments, while a mounted ConfigMap provides the detailed configuration that rarely changes.
Team Onboarding: New team members clone the repository, and the committed config file gives them a working setup immediately. As they learn, they can override settings temporarily with flags before eventually customizing their own config file.
The Cost of Rigidity
Tools that only support one configuration method create unnecessary friction:
- CLI-only tools require unwieldy scripts full of long command invocations
- Environment-only tools force users to pollute their shell environment or create wrapper scripts
- Config-file-only tools make ad-hoc usage painful and complicate containerization
The cost of supporting multiple methods is minimal compared to the flexibility gained.
Start Simple, Grow Thoughtfully
You don’t need to implement every configuration method on day one. A pragmatic approach:
- Start with CLI flags for core parameters
- Add config file support when configuration grows complex
- Layer in environment variable support when users deploy to different environments
- Maintain clear documentation as you add methods
The key is having a plan for supporting all three as your tool matures.
Conclusion
Configuration flexibility isn’t a luxury - it’s a fundamental aspect of building tools that integrate smoothly into diverse workflows and environments. By supporting CLI flags for immediacy, environment variables for context, and config files for persistence, you create tools that adapt to how people actually work.
At Durable Programming, this philosophy guides our open source work and the solutions we build for clients. Whether it’s developer tools like dprs or specialized libraries like abachrome, thoughtful configuration design makes software more robust, more maintainable, and more pleasant to use.
Your users work in different ways, in different environments, with different constraints. Give them the flexibility to configure your tools in whatever way fits their workflow best. They’ll thank you for it.
Interested in building more robust, flexible software? Check out our open source projects at github.com/durableprogramming or reach out to learn how Durable Programming can help your team build better tools.
You may also like...
The Wonder of Rails, Inertia, and Svelte for Web Development
A practical guide to combining Ruby on Rails, Inertia.js, and Svelte to deliver rapid full-stack development and exceptional long-term maintainability.
Export your Asana Tasks as Plaintext
Learn how to export Asana project data to plain text YAML files for long-term accessibility, custom analysis, and freedom from vendor lock-in.
The Importance of Locking Gem Versions in Ruby Projects
Learn why locking gem versions is crucial for Ruby stability, and how to prevent dependency conflicts and deployment surprises across environments.

