Name the common ways teams break a layered architecture in practice (layer-bridging and leakage anti-patterns) and explain why each is harmful.
answer
- smart UI, anemic domain, entity leakage
- upward calls = cycles
- skip-the-layer shortcut bypasses authz
- sinkhole and catch-all util module
- enforce with modules + arch tests
basics
~20 sTypical breakages: controllers containing business rules, database entities used directly as API responses, lower layers calling upward, skipping the domain to hit the database, and pass-through layers that add nothing. Each one couples layers that should change independently.
solid answer
~60 sThe recurring violations are: 1. **Smart UI / logic in the presentation layer** — rules duplicated per endpoint or screen, untestable without HTTP, and inconsistent between clients. 2. **Anemic domain** — entities reduced to getters/setters while services hold all rules, so invariants can be bypassed by any caller. 3. **Entity leakage** — ORM entities or database rows serialized straight to API clients, welding your public contract to your schema and exposing fields you never meant to publish. 4. **Upward dependencies / call-backs into higher layers** — repositories invoking services, creating cycles that break build order, testing, and reasoning. 5. **Layer skipping** in a supposedly strict architecture — usually "just this once" for performance, later used for writes, silently bypassing authorization and invariants. 6. **Sinkhole layers** — pure delegation that costs code and comprehension with no benefit. 7. **Ambient leakage** — framework, transaction, or HTTP concepts (request-scoped context, ORM lazy-loading, vendor exceptions) reaching into the domain. The fix is the same in each case: put the rule where it belongs, map at boundaries, own abstractions at the consumer, and enforce the dependency graph with build modules or architecture tests rather than code review alone.
go deeper
Name two or three concrete violations — logic in controllers, entities returned as API responses, lower layers calling upward — and why each hurts.
Add layer skipping and sinkholes, explain the coupling consequence of each, and mention DTO mapping at the boundary.
Cover semantic/ambient leakage and shared catch-all modules, and propose enforcement via modules and architecture tests rather than review.
Prioritise by change frequency and whether the boundary is published, call out distributed layering as an anti-pattern, and describe fitness functions and governance that keep the graph acyclic across teams.
## Why bridging happens Layering only pays off if the rule holds everywhere. Every exception converts a one-way graph into an ordinary tangle, and tangles cannot be built, tested, or reasoned about incrementally. Violations are rarely malicious: they come from deadline pressure, from frameworks that make the shortcut easiest, and from rules that exist only in documentation. ## The catalogue ### 1. Business logic in presentation ("smart UI") Controllers that compute discounts, enforce eligibility, or decide state transitions. Symptoms: the same rule appears in the web controller and the batch job with subtle differences; tests require an HTTP client; a second client (mobile, CLI) forces copy-paste. Fix: move the decision into a domain type or use case; the controller should translate protocol → call → protocol. ### 2. Anemic domain model Entities are data bags; all behaviour lives in "services". This is not automatically wrong for simple CRUD, but in a rules-heavy domain it means invariants are enforced only if callers remember to call the right service, and the same rule gets re-implemented. Fix: give entities the operations that protect their own invariants; keep services for orchestration across entities. ### 3. Persistence/entity leakage upward Returning ORM entities or raw rows from the API. Consequences: the wire contract is your schema, so a column rename is a breaking public change; you leak internal fields (password hashes, internal flags); lazy-loading triggers queries during serialization; and cyclic object graphs break serializers. Fix: explicit DTOs and mapping at the presentation boundary. Yes, mapping is work — that work *is* the decoupling. ### 4. Upward dependency / reverse call A repository calls a service; an infrastructure adapter imports a controller's type; a domain object reaches for a framework's request context. This introduces a cycle. Legitimate upward *notification* uses contracts owned by the lower layer (events, callbacks) or inverted interfaces owned by the domain. ### 5. Layer skipping under a strict rule A "read-only shortcut" from controller to repository. It bypasses authorization, validation, auditing, and caching that live in the skipped layer, and it multiplies the number of clients of the deepest, most volatile interface. If skipping is genuinely justified, make it explicit policy (for example a separate read/query path) rather than an undocumented exception. ### 6. Sinkhole / pass-through layers Layers whose methods only forward. Each hop costs a class, a test, a mapping, and reader time. Judge by the share of requests that pass through with no added behaviour; a large majority means the layer should be opened or removed. ### 7. Ambient and semantic leakage Subtler than an import: the domain assumes a transaction is already open, relies on ORM lazy-loading to fetch relations, catches vendor-specific exceptions, or reads a thread-local security context. The types look clean but the behaviour is coupled — the domain will misbehave outside its usual host. Fix: pass what you need explicitly (clock, current user, unit of work) and translate vendor errors at the adapter. ### 8. Shared "common" or "util" layer that everything depends on A catch-all module accumulating domain rules, DTOs, and infrastructure helpers becomes a hidden bidirectional coupling point: two layers that appear independent both depend on it and therefore on each other's changes. Fix: split by intent, and be ruthless about what is genuinely shared. ### 9. Distributed layering ("layered microservices") Deploying each layer as its own service so that every business request crosses the network N times. You inherit latency, partial failure, and versioning without gaining independent deployability, because a feature change still touches all of them. Services should be split by capability, not by technical layer. ## Detecting and preventing - **Separate build modules** with declared dependency edges: a violation fails compilation. Strongest available guard. - **Architecture tests** expressing rules over packages ("nothing in `domain` may reference `infrastructure`", "only `web` may reference the HTTP framework"). - **Static dependency analysis** producing a module graph; check for cycles in CI. - **Language visibility**: package-private/internal types so implementations are not reachable across the boundary. - **Fitness functions and review checklists** for the semantic leaks tools cannot see (ambient context, transaction assumptions). ## The judgement call Not every violation is worth fixing immediately. Weigh how often that code changes, how many callers exist, and whether the leak crosses a *published* boundary (public API, cross-team module) — leaks across published contracts are far more expensive than leaks inside one team's module. A pragmatic senior answer names the violation, its cost, and the smallest enforcement that stops it recurring.
- Mapping ORM entities to DTOs feels like duplicated code. How do you justify it?The two models answer different questions: the entity serves storage and invariants, the DTO serves a published contract and its compatibility rules. Fusing them makes every schema change a breaking API change and leaks internal fields. If the models truly never diverge — a small internal CRUD service — sharing them can be a deliberate, documented trade-off; the mistake is doing it by default on a public API.
- How would you stop layering violations from reappearing after you clean them up?Encode the rule where it is checked automatically: separate build modules with explicit dependency edges so violations fail the build, plus architecture tests for package-level rules and a CI cycle check on the module graph. Reserve code review for the semantic leaks tooling cannot see, such as assumed ambient transactions or thread-local context.
saying these in an interview costs you the question
- Serializing database entities directly to API clients and calling the mapping layer 'boilerplate'.
- Treating a repository that calls a service as acceptable because 'it's only for logging'.
- Believing an anemic model is always wrong, including in thin CRUD services.
- Deploying each layer as a separate microservice for 'scalability'.
- Assuming clean imports mean no coupling, while the domain silently depends on an ambient transaction or security context.
- Relying on code review alone to enforce the dependency rule.