Name several architecture smells and the structural metrics used to detect them, and explain the limits of managing architecture by such metrics.
answer
- Smells: cycles, god/hub component, unstable dependency, scattered functionality, hidden DB coupling
- Ca / Ce / I = Ce/(Ca+Ce); A; D = |A + I − 1|
- Main sequence: stable ⇒ abstract; zones of pain and uselessness
- Change coupling from commit history beats structure alone
- Goodhart: metrics as targets get gamed — enforce rules, trend the rest
basics
~20 sArchitecture smells are structural warning signs above the code level: dependency cycles between modules, a god component everything depends on, a shared 'utils' hub, and features scattered across many modules. Metrics such as coupling counts, instability and change coupling help spot them, but they are indicators, not verdicts.
solid answer
~50 sCommon architecture smells: **cyclic dependency** between modules (they must now be understood, built, tested and released together); **god/hub-like component** with very high incoming and outgoing dependencies; **unstable dependency**, where a stable module depends on a volatile one, inverting the Stable Dependencies Principle; **scattered functionality / shotgun surgery**, where one concept lives in many modules; **ambiguous interface**, a single generic entry point hiding many operations; and **implicit cross-module coupling** through shared databases or global state. Detection metrics include afferent/efferent coupling (Ca, Ce), Robert Martin's instability I = Ce/(Ca+Ce), abstractness A, distance from the main sequence D = |A + I − 1|, cycle counts, propagation cost, and — from history rather than structure — change coupling and hotspot churn. Their limits matter: metrics are proxies, need context (a stable, concrete, heavily-depended-on module may be fine), and once targeted become gameable (Goodhart's law). Use them to raise questions, then decide with domain knowledge.
go deeper
Name two or three smells — dependency cycles, a god or shared 'utils' component, features scattered across modules — and say why each makes change harder.
Add the detection metrics: afferent and efferent coupling, instability, cycle detection, and note that thresholds are conventional rather than absolute.
Explain instability, abstractness and distance from the main sequence with the two failure zones, bring in change coupling from version-control history as often more predictive than structure, and discuss what static analysis cannot see.
Argue the governance position: enforce the few rules you genuinely mean as build-failing fitness functions, track the rest as trends with deterioration alerts, cross structure with change history so attention follows real cost, and name Goodhart's law explicitly as the reason not to set metric targets.
## What an architecture smell is A **smell** is a structural pattern that *suggests* a problem without proving one — the architecture-level analogue of a code smell. Architecture smells operate on modules, components, services and their dependencies rather than on statements and functions, so their cost is systemic: they constrain how independently parts can be understood, built, tested, deployed and owned. ## A working catalogue - **Cyclic dependency** — A depends on B which (directly or transitively) depends on A. The cycle collapses into a single de facto unit: it must be understood together, often built and released together, and cannot be tested in isolation. Cycles between modules are the single most cited architecture smell. - **God / hub-like component** — one component with unusually high incoming *and* outgoing dependencies. Every change risks touching it; it becomes a merge bottleneck and a point of ownership contention. The classic instance is the `common`/`shared`/`utils` module that grows to depend on everything. - **Unstable dependency** — a component that many things depend on (so it should be stable) itself depends on a volatile component. Every churn in the volatile part propagates upward. This inverts the **Stable Dependencies Principle**: depend in the direction of stability. - **Scattered functionality / shotgun surgery** — one business concept implemented across many modules, so a single logical change requires coordinated edits everywhere. Usually a symptom of boundaries drawn along technical layers rather than domain capability. - **Ambiguous interface** — a component exposing one generic entry point (`handle(request)`, `execute(command)`) that hides many unrelated operations, so callers cannot tell what it offers and static analysis cannot see real dependencies. - **Implicit / hidden coupling** — components integrating through a shared database schema, shared mutable global state, or a shared file format, so the dependency exists but is invisible to dependency analysis. Especially dangerous because automated checks report a clean graph. - **Dense structure** — a dependency graph whose edge count approaches the fully-connected case; nothing can be reasoned about locally. - **Feature concentration** — one module implementing many unrelated capabilities, so it changes for many independent reasons (module-scale violation of the Single Responsibility Principle). ## Metrics used for detection **Structural (from the dependency graph):** - **Afferent coupling (Ca)** — number of external components depending *on* this one (incoming; responsibility). - **Efferent coupling (Ce)** — number of external components this one depends *on* (outgoing; dependence). - **Instability I = Ce / (Ca + Ce)**, in [0,1]. I = 0 is maximally stable (many depend on it, it depends on nothing); I = 1 is maximally unstable. The Stable Dependencies Principle: dependencies should point toward lower I. - **Abstractness A** = abstract types ÷ total types in the component, in [0,1]. - **Distance from the main sequence D = |A + I − 1|**. The main sequence is the line A + I = 1: stable components should be abstract (so they can be extended without modification), unstable ones concrete. Large D flags the two problem zones — the *zone of pain* (stable and concrete: everything depends on it and it cannot be extended without modifying it) and the *zone of uselessness* (abstract with nothing depending on it). - **Cycle count / size of the largest strongly connected component** in the module graph. - **Propagation cost** — the fraction of components reachable from an average component through transitive dependencies; a direct measure of how far a change can ripple. **Historical (from version control — often more predictive than structure alone):** - **Change coupling / logical coupling** — files or modules repeatedly committed together despite no static dependency. Strong evidence of a misplaced boundary or hidden coupling. - **Hotspot churn** — change frequency crossed with complexity, identifying where structural cost is actually being paid. - **Contributor spread** — many teams editing one component signals ownership ambiguity and predicts defects. ## The limits — why metrics do not decide 1. **Proxies, not truth.** High Ca is expected and healthy for a well-designed shared domain library; the metric cannot tell a good hub from a god component. Only context — what the component *is for*, and how often it changes — distinguishes them. 2. **Thresholds are conventional.** There is no universal cutoff for "too much coupling". Values are meaningful relatively (against this codebase's own distribution and its trend over time), not absolutely. 3. **Goodhart's law.** When a measure becomes a target, it ceases to be a good measure. Targeting instability produces pointless interfaces; targeting cycle counts produces artificial indirection layers that break the static cycle while keeping the conceptual one. The system scores better and is no easier to change. 4. **Blind spots.** Static analysis misses coupling through shared databases, message payload formats, reflection, dynamic dispatch, configuration, and runtime service discovery. A clean graph can coexist with a tangled system. 5. **They locate, they do not diagnose.** A metric says "look here"; whether the structure is wrong, and what the right structure would be, requires domain understanding of why the component changes. 6. **Cost of remediation is invisible to the metric.** A high-D component nobody touches that would take three months to split is not a priority; metrics carry no notion of principal or of change traffic unless deliberately crossed with history. ## How to use them well Track a small set as **trends with alerts on deterioration**, not as scores to optimise. Encode the few rules you genuinely mean — no cycles between these modules, this module may not depend on that one — as build-failing checks (fitness functions), because a rule enforced at commit time prevents erosion far more reliably than a dashboard reviewed quarterly. Cross structural metrics with change history so attention lands where interest is actually paid, and always let a human decide what the target structure should be.
- Why are dependency cycles between modules considered so serious?A cycle turns the involved modules into one unit for reasoning, building, testing, releasing and often ownership. You cannot understand or change one without the others, independent deployment is lost, test setup expands, and the cycle tends to attract more members over time because the boundary has already been conceded.
- Robert Martin's distance from the main sequence uses D = |A + I − 1|. What are the two failure zones it identifies?The zone of pain — low instability with low abstractness: many components depend on it and it is concrete, so it cannot be extended without modifying it and every change is expensive and wide-reaching. And the zone of uselessness — high abstractness with high instability: abstractions nobody depends on, which is dead generality. Both are signals to investigate, and some inhabitants of the zone of pain (a stable string library, for instance) are perfectly acceptable.
- Static dependency analysis reports a clean graph for a system that is nevertheless painful to change. What might it be missing?Coupling that is invisible to static analysis: components integrating through a shared database schema, shared mutable state, message or file formats agreed by convention, reflection or dynamic dispatch, configuration and feature flags, and runtime service discovery. Change coupling from commit history is the practical detector — modules that keep changing together despite no static edge are coupled through something the analyser cannot see.
saying these in an interview costs you the question
- Treating metric thresholds as absolute pass/fail rather than as relative, trend-based indicators
- Assuming any component with high incoming dependencies is a god component
- Optimising instability or cycle counts directly, producing indirection that games the measure without improving changeability
- Believing a clean static dependency graph proves the system is loosely coupled, ignoring shared databases and global state
- Reporting smells without crossing them against change frequency, so effort goes to structures nobody touches
- Presenting metric dashboards as decisions instead of as prompts for human architectural judgement