Why Great Systems Remove Choices, Then Reintroduce Them Carefully
Hatched by John Smith
Jun 14, 2026
11 min read
2 views
71%
The hidden question behind style and dependency
What if the hardest part of building software is not writing code, but deciding where choice belongs?
That question shows up in two places that look unrelated at first glance. In one, a formatter tries to erase endless arguments about whitespace, line wrapping, and quote style. In the other, a web framework gives you a structured way to supply dependencies so your application can be assembled cleanly. One is about making code look the same. The other is about making code behave predictably. But both are really about the same deeper problem: how to design systems so humans spend less time negotiating mechanics and more time solving the actual problem.
This is not just a question of convenience. It is a question of power. Every option you expose creates freedom, but also introduces new coordination costs. Every dependency you inject improves testability and modularity, but also adds a new layer of architecture that can become opaque if handled carelessly. The best systems do not maximize choice. They allocate choice deliberately.
That distinction matters because many teams confuse flexibility with maturity. In practice, the opposite is often true. A system becomes more mature when it learns which decisions should be fixed, which should be configurable, and which should be inferred automatically.
Choice is not free, it is a debt with interest
Most teams think of options as kindness. “Let people choose,” the instinct says. “Different teams have different preferences.” That sounds reasonable, and sometimes it is. But every option has a hidden price tag.
An option does not merely give users control. It also creates a new surface area for debate, documentation, onboarding, compatibility, and future maintenance. If a tool allows three formatting styles, people will compare them. If a framework allows six ways to inject a service, people will spend afternoons arguing about which one is “more correct.” The original problem disappears, replaced by meta-discussion about the mechanism.
A useful mental model is this: an option is a miniature fork in the road that every future reader must walk past. Even if they never change the setting, they must notice it, understand it, and remember that it exists. Multiply that by dozens of toggles, and the cognitive load becomes part of the product.
This is why the healthiest defaults are often opinionated. An opinionated system says, “We have reduced the space of choices so you can move faster.” That is not authoritarianism. It is a design strategy that recognizes a basic truth: human attention is scarcer than code.
Think of a restaurant menu. A ten page menu may seem generous, but it often signals indecision in the kitchen. A focused menu says the chef has chosen a style, optimized the workflow, and can deliver consistently. Software systems are no different. Too many options may look empowering, yet they frequently reveal that the system has not decided what it wants to be.
The paradox of frameworks: freedom through constraint
Now consider dependency injection. At first glance, it seems to move in the opposite direction. Instead of reducing choice, it formalizes how components receive what they need from the outside. That sounds like more abstraction, more indirection, more machinery. Why would this help?
Because not all choice is the same. There is a difference between end user choice and architectural choice.
A formatter’s users do not want to think about how code should be printed every time they save a file. A developer using a framework does not want every function to hardcode its own database client, logger, or HTTP adapter. In both cases, the system is healthier when its internal pieces are not responsible for assembling themselves in ad hoc ways.
Dependency injection creates a powerful separation: components declare what they need, while composition happens elsewhere. This does two things at once. First, it reduces coupling. Second, it moves decisions to a more appropriate layer, where they can be made once instead of repeatedly.
Here is a simple analogy. Imagine a stage play where every actor also has to build the set, manage the lighting, and find their own costume in the middle of the performance. That is not flexibility, it is chaos. Dependency injection is the production crew. It lets the actors focus on acting by moving setup concerns to a dedicated place.
The same pattern appears in good code. A route handler should usually not decide how to instantiate a payment gateway or where to obtain configuration secrets. Those decisions belong in composition, bootstrap, or infrastructure code. The handler should say, in effect, “I need a payment service.” The system should respond, “Here is one.”
Good architecture does not eliminate dependencies. It makes them visible, replaceable, and centrally managed.
That sentence captures the bridge between the two worlds. Opinionated formatting centralizes one kind of decision so teams can stop discussing it. Dependency injection centralizes another kind of decision so components can stop making it for themselves. In both cases, the goal is not less structure. It is better placed structure.
The real design problem: deciding where a decision should live
This leads to a more precise framework: every decision in a software system should answer three questions.
- Who cares about this decision?
- How often is this decision likely to change?
- What is the cost of exposing this decision to everyone?
If a decision is low value to most users, high cost to document, and likely to create disagreement, it should probably be fixed. Formatting rules are a classic example. Most teams do not need to customize every syntactic preference. They need consistency.
If a decision varies across environments, tests, or integrations, it should probably be injectable. Database clients, feature flag providers, external APIs, and clocks are common examples. The code should depend on the abstract need, not the concrete mechanism.
If a decision is genuinely part of the product experience, then it may belong as a user facing option. Even then, the bar should be high. The question is not “Can we make this configurable?” The question is “Does exposing this choice make the system meaningfully better for the people who use it?”
This framework cuts through a common trap: treating every painful question as if it deserves a setting. In reality, some pain should be removed by design, some by architecture, and some by policy. The mistake is to confuse all three.
A code formatter does not ask whether every team wants to align braces differently because that would defeat its purpose. A dependency system does not ask every function to build its own dependencies because that would defeat modularity. In both cases, restraint is a feature, not a limitation.
The question every good system eventually asks is: what should be automatic, what should be injectable, and what should be configurable?
A practical model: defaults, seams, and escape hatches
The cleanest way to understand the relationship is to think in three layers.
1. Defaults: remove the debate
Defaults are for decisions you want to settle once. They are strongest when the right answer is stable, widely applicable, and not central to the user’s goals. Formatting rules belong here. They create a shared baseline that reduces friction and makes collaboration easier.
A good default does more than save time. It establishes a norm. It tells the team what the system considers normal. This is powerful because teams often underestimate the social value of shared defaults. If everyone can opt out, no one truly shares a standard.
2. Seams: make change possible without rewriting everything
A seam is a place where one part of the system can be swapped without disturbing the rest. Dependency injection creates seams. Instead of reaching directly for a database or clock, a component accepts them as dependencies. That means tests can use fakes, local runs can use stubs, and production can use real implementations.
This is where flexibility earns its keep. Not every variation is stylistic. Some variation is structural. If your code cannot be tested without touching the network, or cannot run in two environments without branching everywhere, it is too rigid.
3. Escape hatches: preserve power for exceptional cases
No opinionated system can be perfect for every case. That is why escape hatches exist. A formatter may need compatibility options. A dependency system may need custom providers or manual wiring in unusual cases. The important thing is that exceptions are treated as exceptions.
An escape hatch is healthy only when it is expensive enough to discourage casual use, but available enough to prevent the system from becoming a prison. This balance is subtle. Too easy, and the exception becomes the norm. Too hard, and the system breaks down under real world edge cases.
The mark of a mature system is not that it has no options, but that its options have a purpose, a cost, and a clear boundary.
This is the missing discipline in many tools and codebases. Teams often add options because they are uncomfortable making a choice. Then they add more options to reconcile the confusion caused by the first options. Soon the system is no longer opinionated, just indeterminate.
Where teams go wrong: confusing flexibility with responsibility
A subtle failure mode appears when teams believe that giving choice transfers responsibility to users. In reality, it often transfers decision fatigue.
A developer choosing between ten formatter settings is not being empowered. They are being asked to become a policy expert. A programmer choosing between four injection styles is not necessarily being helped. They may instead be navigating an ecosystem whose abstractions are poorly aligned with how people actually build software.
The best systems do the hard thinking in advance. They absorb complexity at the center so that complexity does not spread to the edges. This is not a universal principle, but it is a strong default for infrastructure and tooling.
Here is a concrete example. Suppose a team is building an API server. Without disciplined dependency handling, route handlers may directly instantiate repositories, loggers, and clients. The result is code that is harder to test, harder to reuse, and harder to reason about. With a well designed injection strategy, handlers become smaller and clearer. The wiring moves into a composition layer, where environment specific decisions are concentrated.
Now imagine the same team allowing every developer to choose a personal formatting style. Suddenly code review becomes partly about aesthetics. Merge conflicts become harder to scan. New contributors spend time learning local preferences rather than domain logic. The team has not gained freedom. It has imported noise.
The lesson is not that all options are bad. The lesson is that every option should justify its existence by reducing net complexity, not merely by satisfying a preference.
The synthesis: software quality is mostly decision quality
The deepest connection between opinionated formatting and dependency injection is that both are disciplines of decision quality.
A well designed formatter says: some decisions are so low value, so universally distracting, and so unlikely to deserve debate that the tool should make them for you. A well designed application architecture says: some decisions are so context specific, so environment dependent, and so expensive to hardcode that the system should make them outside the component that uses them.
These are not opposite philosophies. They are complementary. One removes unnecessary freedom from the surface. The other preserves necessary freedom at the seams. Together, they create a system in which human judgment is spent where it matters most.
This is the real art of engineering. Not maximizing options. Not eliminating options. But sorting decisions by their rightful home.
That can be applied beyond formatting and dependency injection. Product design, API design, organizational process, even meeting culture all benefit from the same question: should this decision be automatic, local, or user controlled? The more often you ask it, the less likely you are to build systems that are technically elegant but socially exhausting.
A team with too many options becomes a committee. A team with thoughtful defaults and clean seams becomes a machine for progress.
Key Takeaways
-
Treat every option as a cost, not a gift. Ask whether the choice reduces real complexity or simply relocates it.
-
Use defaults to eliminate pointless debate. If a decision is low value and high noise, fix it centrally and stop re arguing it.
-
Use dependency injection to create seams, not ceremony. Inject things that vary by environment or need to be swapped in tests, not everything for its own sake.
-
Separate user facing choices from architectural choices. What end users should control is different from what internal components should be responsible for.
-
Add escape hatches only with clear intent. Exceptions should exist, but they should remain exceptional, documented, and easy to audit.
Conclusion: the highest form of flexibility is selective rigidity
We usually praise flexibility as if it were the highest engineering virtue. But flexible systems often become fragile when every decision is left open. The more profound achievement is selective rigidity: knowing what to freeze so the rest can move freely.
That is why a good formatter can feel liberating even though it removes options, and why good dependency design can feel elegant even though it introduces structure. Both are forms of relief. They relieve people from the burden of making the same decision repeatedly in the wrong place.
So the next time you are tempted to add a toggle, a setting, or one more way to wire a component, pause and ask: is this a feature, or is it a failure to decide? The answer will tell you whether you are designing for human flourishing or merely distributing complexity.
The most powerful systems are not the ones that let you do anything. They are the ones that help you stop wasting thought on the things you should never have had to decide in the first place.
Sources
Hatch New Ideas with Glasp AI 🐣
Glasp AI allows you to hatch new ideas based on your curated content. Let's curate and create with Glasp AI :)
Start Hatching 🐣