What concrete costs does strictly enforcing inward-pointing dependencies (ports/interfaces for every outward call) impose on a codebase, and under what circumstances would you deliberately not enforce it?
answer
- interface-itis vs coupling risk
- YAGNI vs DIP tension
- cost is certain, benefit is speculative
- composition root is exempt
- just-in-time abstraction / rule of three
basics
~20 sEvery interface you add 'just in case' is extra code to write, read, and navigate, and it can slow a small team down for no real benefit if there's genuinely only ever going to be one implementation. Skip the strict discipline for small, short-lived, or truly single-technology apps where the 'swap' you're protecting against will never happen.
solid answer
~40 sCosts: more files and indirection per feature, an extra mapping layer between domain shapes and infrastructure shapes, slower onboarding because a reader has to jump through an interface to find the real behavior, and genuine risk of speculative generality — building a seam for a substitution that never materializes. I'd relax the discipline for small internal tools, early-stage prototypes/MVPs where the point is to learn whether the product is worth building, single-database CRUD services with no realistic swap-out plan, and one-off scripts — anywhere the cost of the abstraction is certain and the benefit is speculative. Even in disciplined codebases, it's common to allow glue code (composition roots, DTO mappers) to depend outward pragmatically, since its whole job is bridging the outer world to the inner one.
go deeper
Can name 'it's extra files/code' as a cost when prompted, without necessarily generalizing to when it's worth it.
Can identify obvious cases (a throwaway script, a tiny internal tool) where full inversion is overkill.
Articulates the YAGNI-vs-DIP tension explicitly, gives a decision heuristic, and names interface-itis as a real, symmetric failure mode to the coupling problem.
Sets or influences team-wide defaults/conventions balancing the two failure modes across many teams and codebases, and knows how cheaply the refactor can happen later if done incrementally.
## The fixed cost of a seam Every port-and-adapter seam has a **fixed cost that gets paid regardless** of whether the seam is ever used for its intended purpose. Concretely: for each outward dependency a team decides to invert, someone - writes an interface - writes (or already has) a concrete implementation of it - writes the code that wires the two together at a composition root - often writes a translation/mapping layer between the inner model's shape and the outer system's shape A reader trying to understand what actually happens when an order is saved now has to follow a call from a use case, through an interface, to a class located in a different package, possibly resolved by a DI container at runtime rather than visible from a simple 'go to definition.' None of this is expensive in isolation, but multiplied across every repository, every external API call, every file write in a codebase, it adds up to meaningfully more files, more indirection, and slower onboarding for new engineers who have to learn where 'the real implementation' lives for each interface. ## The tension with YAGNI This cost is worth naming honestly because it's in real tension with a competing, equally legitimate principle: **YAGNI** ('you aren't gonna need it') and the well-known observation that **duplication is far cheaper to live with than the wrong abstraction**. An interface introduced 'in case we need to swap databases someday' that never gets a second implementation is a pure cost with no realized benefit — it's **speculative generality**, and it's arguably a bigger design smell than the coupling it was meant to prevent, because at least a direct dependency is honest about what the code actually does. A **'just-in-time'** approach to this kind of abstraction is common in practice: start with the direct, simple dependency, and only invert it once you have a second concrete reason to (a second real implementation, or a genuine need to unit-test without infrastructure) rather than pre-emptively. ## When I would relax it Concretely, I'd relax strict inward-pointing discipline in a handful of recurring situations. - **Early-stage prototypes and MVPs**, where the team's actual risk is 'nobody wants this product,' not 'we might swap Postgres for DynamoDB' — the fastest path to learning that trumps architectural purity, and the code is often thrown away or heavily rewritten regardless. - **Small internal tools and one-off scripts** with a single, known-forever technology choice and a tiny number of maintainers who can hold the whole thing in their head — the indirection buys nothing because there's no meaningful complexity to hide it from. - **Simple CRUD services** with no plausible multi-database or multi-provider future, where the persistence technology is treated as effectively part of the platform — here, testing against a real, fast, disposable test database can be a perfectly good substitute for the isolation an interface would buy, without paying the interface's tax on every change. - **And I'd always exempt glue/composition-root code** — bootstrap functions, DI configuration classes, mapping/adapter code whose entire purpose is bridging inner and outer worlds — from the rule, since forcing that code to also point inward is incoherent; its job is to know about both sides. ## The two failure modes The failure mode on the over-enforcement side is **'interface-itis'**: a codebase where nearly every class has a matching interface/implementation pair with exactly one implementation that will, realistically, ever exist, where every change touches twice as many files as it actually requires, and where new team members spend their first weeks just learning to navigate indirection rather than reading straightforward code. It's the mirror image of the under-enforcement failure mode (business logic wired directly to a database class, unable to be tested or swapped) — both are real, and a senior engineer's job is judging, case by case, which risk is more expensive for the specific piece of code in front of them, rather than applying either extreme uniformly across a whole codebase. ## A reasonable default A reasonable default in practice: apply strict inward direction at genuine architectural seams — the persistence layer, external payment/notification providers, anything with more than one real or planned implementation, or anything you need to test without infrastructure — and allow direct, simple dependencies for stable, single-implementation, low-risk technical choices. **The judgment call itself, not a blanket rule in either direction, is the actual skill being exercised here.**
- How would you decide, for a specific dependency, whether it's worth inverting versus leaving direct?Ask two questions: is there a real, non-speculative second implementation or substitution need (a second provider, a genuine need to unit-test without infrastructure), and is the dependency at a boundary likely to actually change? If both answers are yes, invert it; if the honest answer is 'maybe someday,' default to the direct dependency and revisit once the need is concrete.
- If a team starts with a direct dependency and later needs to invert it, how costly is that refactor typically?Usually cheap if the direct dependency was small and localized — introducing the interface, renaming the concrete class, and adding wiring is a mechanical, low-risk refactor most IDEs support directly. It gets expensive only if the direct dependency was allowed to leak deeply into many call sites first, which argues for doing the extraction as soon as a real second need appears rather than waiting.
- Is 'interface-itis' purely a stylistic complaint, or does it have a measurable cost?It has a measurable cost in change velocity and onboarding time — every feature that touches a needlessly-inverted dependency requires editing and reviewing more files than the underlying logic change requires, and new engineers spend real time learning to trace calls through indirection that resolves to a single, unchanging implementation.
Building a removable wall between every two rooms in a house 'in case you rearrange furniture someday' — sometimes worth it for a room you'll actually reconfigure, wasteful for a closet that will always be a closet.
saying these in an interview costs you the question
- Insists every dependency must always be inverted with no exceptions
- Can't name a single situation where a direct dependency is the right call
- Doesn't recognize that unused interfaces have a real, ongoing maintenance cost
- Treats YAGNI and DIP as if only one of them is ever correct