- Chapter
- Chapter 1 — What Is Software Design?
- Prerequisites
- Topics 1.1 and 1.2
- You will be able to
- Assess a design with coupling and cohesion, recognise complexity and duplication, diagnose rigidity, fragility and immobility, and choose refactorings that address each
- Covers
- Coupling · Cohesion · Complexity · Duplication · Rigidity · Fragility · Immobility
On this page
- 1. Concept / Definition
- 2. Why It Exists
- 3. Core Concepts
- 4. How It Works Internally — From Causes to Symptoms
- 4A. Deep Dive — Measuring Design Quality
- 5. APIs / Features / Syntax — Java Tools Against Poor Design
- 6. Practical Examples
- 7. Common Problems & Mistakes
- 8. Performance & Production Considerations
- 9. Architecture & Design Considerations
- 10. Interview Questions & Answers
1Concept / Definition
Good design keeps the cost and risk of change low; poor design raises them. The difference can be described with two structural properties — coupling and cohesion — two accumulating burdens — complexity and duplication — and three observable symptoms — rigidity, fragility and immobility.

| Category | Item | Good | Poor |
|---|---|---|---|
| Structure | Coupling | Few, narrow, stable dependencies | Many, broad, volatile dependencies |
| Structure | Cohesion | Everything in a module serves one purpose | Unrelated responsibilities mixed |
| Burden | Complexity | Close to the problem's essential complexity | Accidental complexity dominates |
| Burden | Duplication | Each piece of knowledge in one place | Same rule in several places |
| Symptom | Rigidity | Changes are local | Changes cascade |
| Symptom | Fragility | Changes break only what they touch | Changes break unrelated places |
| Symptom | Immobility | Parts reusable elsewhere | Parts inseparable from context |
2Why It Exists
Larry Constantine introduced coupling and cohesion in the late 1960s as the first objective design criteria; they remain the most useful today. Robert C. Martin's Design Principles and Design Patterns (2000) catalogued the symptoms of "rotting" design — rigidity, fragility, immobility and viscosity — to give teams language for the decay they experienced. Duplication (DRY, The Pragmatic Programmer, 1999) and complexity (Brooks, No Silver Bullet, 1986; McCabe, 1976) complete the picture.
3Core Concepts
3.1 Coupling
Coupling is the degree of interdependence between modules: how much one must know about another and how likely a change in one forces a change in the other.
// Tight coupling: knows internals of Customer and the concrete repository
class DiscountService {
MySqlCustomerRepository repo = new MySqlCustomerRepository();
BigDecimal discount(long id) {
Customer c = repo.find(id);
if (c.orders.size() > 10 && c.address.country.equals("DE")) {
return new BigDecimal("0.05");
}
return BigDecimal.ZERO;
}
}
// Looser: depends on an abstraction and asks Customer instead of inspecting it
class DiscountService {
private final Customers customers;
DiscountService(Customers customers) { this.customers = customers; }
Percentage discount(CustomerId id) {
return customers.byId(id).isLoyal() ? Percentage.of(5) : Percentage.ZERO;
}
}| Indicator of tight coupling | Why it hurts |
|---|---|
Reaching through objects (a.getB().getC().doX()) | Knows structure of distant objects (Law of Demeter) |
new of concrete collaborators | Cannot substitute; hard to test |
| Shared mutable global state | Hidden interactions |
| Boolean/flag parameters steering logic | Caller knows callee's internals |
3.2 Cohesion
Cohesion is the degree to which the elements inside a module belong together — serve a single, well-defined purpose. High cohesion means a module changes for one reason.
// Low cohesion: unrelated responsibilities
class CustomerManager {
void register(Customer c) { }
void sendNewsletter() { }
byte[] exportToExcel() { return null; }
void recalcLoyaltyPoints() { }
}
// High cohesion: one purpose each
class CustomerRegistration { void register(Customer c) { } }
class NewsletterSender { void send(Newsletter n) { } }
class CustomerExport { byte[] toExcel(List<Customer> cs) { return null; } }
class LoyaltyProgram { Points recalc(Customer c) { return null; } }Names are a cheap cohesion test: Manager, Helper, Utils, Processor often hide several purposes (details in Topic 3.2).
3.3 Complexity
Complexity is anything in the structure of software that makes it hard to understand and modify (Ousterhout, A Philosophy of Software Design). Some is essential (from the problem), much is accidental (from our solution).

| Source of accidental complexity | Example |
|---|---|
| Deep nesting and long methods | 300-line method with 6 nested ifs |
| Unnecessary indirection | Factory creating a builder creating a strategy with one implementation |
| Hidden dependencies | Static calls, thread-locals, reflection |
| Inconsistent conventions | Three ways to do the same thing in one codebase |
| Primitive obsession | String for email, IBAN, currency — rules scattered |
Complexity is examined in depth in Chapter 4 (cyclomatic and cognitive complexity).
3.4 Duplication
Duplication means the same knowledge — a rule, algorithm, decision or data definition — is expressed in more than one place. When the knowledge changes, every copy must change; missed copies become bugs.
// Duplicated knowledge: "orders over 50 EUR ship free" in three places
// checkout
if (order.total().compareTo(new BigDecimal("50")) >= 0) freeShipping = true;
if (cart.subtotal() >= 50.0) banner.show("Free shipping!"); // UI
if (inv.amount().doubleValue() > 50) shippingLine = 0; // invoicing (note: > not >=)
// Single source of truth
public final class ShippingPolicy {
private static final Money FREE_THRESHOLD = Money.eur("50.00");
public boolean isFree(Money amount) {
return amount.isGreaterThanOrEqual(FREE_THRESHOLD);
}
}Note the subtle inconsistency (> vs >=) — the typical cost of duplicated knowledge. Not all similar-looking code is duplication: two rules that look alike but change for different reasons should stay separate (Chapter 5, DRY).
3.5 Rigidity
Rigidity is the tendency of software to be hard to change: a simple change forces a cascade of changes in dependent modules. Estimates balloon because nobody can predict the extent of the change.

| Cause | Typical fix |
|---|---|
| Concrete dependencies everywhere | Depend on abstractions (Dependency Inversion) |
| Decisions not hidden (e.g. data format known by all) | Information hiding (Topic 2.2) |
| Wide interfaces used by many | Narrow, role-specific interfaces |
3.6 Fragility
Fragility is the tendency to break in many places when changed — often in areas with no conceptual relationship to the change. Fragile code destroys confidence; teams stop refactoring and the design decays further.
// Fragile: two classes silently share the meaning of a magic status code
class OrderRepository { void markShipped(long id) { update(id, 3); } }
class ReportService { long shippedCount() { return countByStatus(3); } }
// Someone renumbers statuses → reports silently wrong, no compile error.
// Robust: a shared, typed concept
enum OrderStatus { DRAFT, SUBMITTED, PAID, SHIPPED }| Cause | Typical fix |
|---|---|
| Hidden coupling via shared data, magic values, conventions | Explicit types, single definitions |
| Global mutable state | Encapsulated, injected state |
| Missing tests at boundaries | Contract and characterization tests |
3.7 Immobility
Immobility is the inability to reuse a part in another system or context because it drags along too many dependencies or assumptions. The effort to separate it exceeds the effort to rewrite it, so logic is duplicated instead.
// Immobile: tax rules entangled with HTTP, JPA and Spring
@RestController
class TaxController {
@Autowired EntityManager em;
@GetMapping("/tax") BigDecimal tax(@RequestParam long orderId) {
Order o = em.find(Order.class, orderId);
/* 80 lines of tax rules using HttpServletRequest locale... */
return null;
}
}
// Mobile: pure domain logic usable from web, batch, tests or another service
public final class TaxCalculator {
public Money tax(Money net, Region region, LocalDate date) {
/* rules only */ return null;
}
}
4How It Works Internally — From Causes to Symptoms
| Cause | Leads to symptom |
|---|---|
| High coupling | Rigidity (cascades), fragility (hidden breakages), immobility (can't extract) |
| Low cohesion | Rigidity (many reasons to change), fragility (unrelated code co-located) |
| Duplication | Fragility (missed copies), rigidity (many edits) |
| Accidental complexity | All three; plus slow understanding |
Design decay is usually gradual: each shortcut is locally reasonable; together they produce a "big ball of mud" (Foote & Yoder, 1997). Continuous refactoring reverses the trend in small steps.
4ADeep Dive — Measuring Design Quality
4A.1 Metrics
| Metric | Measures | Tool | Caution |
|---|---|---|---|
| Efferent/afferent coupling (Ce/Ca), instability | Coupling between packages | JDepend-style, Structure101, jdeps | Interpret per package role |
| LCOM4 | Cohesion of a class | IDE plugins, Sonar plugins | Records/DTOs skew results |
| Cyclomatic complexity | Paths through a method | SonarQube, PMD | Does not capture readability |
| Cognitive complexity | Understandability | SonarQube | Heuristic |
| Duplicated lines/blocks | Textual duplication | SonarQube, PMD CPD | Misses conceptual duplication |
| Dependency cycles | Package tangles | ArchUnit, jdeps | Zero cycles is achievable |
4A.2 Behavioural (change-based) analysis
Static metrics describe structure; version-control history shows how it behaves under change:
| Analysis | Reveals |
|---|---|
| Hotspots (change frequency × complexity) | Where poor design costs most |
| Change coupling (files changed together) | Hidden coupling not visible in imports |
| Code age and ownership | Knowledge risks |
// Enforcing a design rule so the good structure survives
@ArchTest
static final ArchRule noCycles =
slices().matching("com.acme.(*)..").should().beFreeOfCycles();5APIs / Features / Syntax — Java Tools Against Poor Design
| Problem | Java feature / tool |
|---|---|
| Coupling to concretions | Interfaces, constructor injection, JPMS exports |
| Low cohesion | Packages by feature, small classes, records for data |
| Duplication of rules | Value objects, enums with behaviour, single policy classes |
| Magic values (fragility) | Enums, sealed types, constants in one place |
| Complexity | Pattern matching switch replacing if-chains, streams for collection logic (judiciously) |
| Cycles | ArchUnit beFreeOfCycles(), JPMS (no cyclic requires) |
6Practical Examples
6.1 Diagnosing a change request
Request: "Add Swiss francs as a supported currency."
| Observation | Diagnosis |
|---|---|
| Currency codes as strings in 40 files | Duplication, primitive obsession |
| Formatting logic in UI, reports and PDFs separately | Duplication, low cohesion |
| Estimate unclear: "somewhere between 3 and 20 days" | Rigidity |
| Last similar change broke invoice PDFs | Fragility |
Remedy: introduce Money/Currency value types, a single formatting service, and characterization tests before migrating call sites.
6.2 Enum with behaviour instead of scattered switches
public enum ShippingSpeed {
STANDARD(Duration.ofDays(5), Money.eur("0")),
EXPRESS(Duration.ofDays(1), Money.eur("9.90"));
private final Duration eta;
private final Money fee;
ShippingSpeed(Duration eta, Money fee) { this.eta = eta; this.fee = fee; }
public LocalDate deliveryDate(LocalDate from) { return from.plusDays(eta.toDays()); }
public Money fee() { return fee; }
}Adding a speed is one change in one place instead of editing every switch on speed.
6.3 Law of Demeter refactoring
// Before: reaching through structure
String city = order.getCustomer().getAddress().getCity();
// After: ask the object that has the knowledge
String city = order.shippingCity();6.4 Design review questions
| Question | Targets |
|---|---|
| "Which modules change if X changes?" | Rigidity, coupling |
| "What could break that isn't obviously related?" | Fragility |
| "Could we use this in the batch job without the web layer?" | Immobility |
| "Where else is this rule written?" | Duplication |
| "Can you explain this class in one sentence?" | Cohesion |
7Common Problems & Mistakes
- Chasing metrics (e.g. zero duplicated lines) at the cost of clarity.
- Over-DRYing: merging code that looks similar but changes for different reasons, creating coupling.
- Decoupling by indirection only: interfaces everywhere while shared mutable state remains.
- Ignoring change coupling: modules that always change together should perhaps be one module.
- Big-bang rewrites instead of incremental refactoring toward better structure.
8Performance & Production Considerations
| Design issue | Production effect |
|---|---|
| Fragility | Regressions after deployments; higher change failure rate |
| Rigidity | Slow hotfixes during incidents |
| Duplication | Inconsistent behaviour between channels (web vs batch) |
| Complexity | Longer incident diagnosis (MTTR) |
Design quality becomes visible in operational metrics, which is also how to make the case for improving it. Track change failure rate and mean time to restore per module or service alongside lead time and deployment frequency: fragile areas show up as rollbacks and hotfixes clustered around the same components, and rigid ones as long lead times for small changes. Accidental complexity lengthens incident diagnosis, so keep critical paths readable and instrument them with structured logs, Micrometer metrics and traces that name domain operations. Duplicated rules between web, batch and integration channels surface as inconsistent customer outcomes and reconciliation work, so prefer one shared policy component. Correlate post-mortem findings with hotspot data to direct refactoring where it reduces operational risk.
9Architecture & Design Considerations
- The same properties apply at every scale: classes, packages, modules, services.
- Distributed systems amplify coupling: a tight coupling between services costs latency and availability, not just edit effort.
- Guardrails: enforce cycles-free packages and layering in CI; track hotspots.
- Continuous refactoring as part of each feature beats periodic clean-up projects.
10Interview Questions & Answers
What are coupling and cohesion?
Coupling is the degree to which one module depends on another: how much it must know about the other's internals and how likely a change there forces a change here. Cohesion is the degree to which the elements inside one module belong together and serve a single purpose. Good design aims for low coupling between modules and high cohesion within them, because then each change lands in one place. A DiscountService that instantiates MySqlCustomerRepository and reads customer.orders directly is tightly coupled; a CustomerManager that registers customers, sends newsletters and exports Excel files has low cohesion. The two usually move together: splitting an incohesive class tends to reduce coupling as well.
What is rigidity?
Rigidity is the tendency of software to resist change: a simple modification forces a cascade of further changes in dependent modules, so the real size of any change is unpredictable. The cause is usually concrete dependencies everywhere and design decisions that are not hidden, such as a data format or currency representation known by dozens of classes. The visible symptom shows up in planning rather than in code: estimates like somewhere between three and twenty days, and managers reluctant to approve even small requests. The typical fixes are depending on abstractions, hiding volatile decisions behind one module, and narrowing wide interfaces so that fewer callers are affected by each change.
How is fragility different from rigidity?
Rigidity means a change is large: you know you must edit many modules, so the work is slow but the impact is visible. Fragility means a change breaks things you did not touch and often did not know were related, so the cost appears later as regressions. Rigidity comes from explicit dependencies; fragility usually comes from hidden coupling, such as two classes sharing the meaning of a magic status code, global mutable state or duplicated rules with subtle differences. A rigid system stays red until every caller is fixed; a fragile one compiles green and fails in production. Fix fragility with explicit types, single definitions of rules and tests at boundaries.
What is immobility and what causes it?
Immobility is the inability to reuse a component in another context because separating it from its current surroundings costs more than rewriting it. The cause is entanglement: domain logic mixed with framework, transport and persistence code, or built on context-specific assumptions such as an HTTP request locale or a particular database schema. A tax calculation inside a Spring controller that uses EntityManager and HttpServletRequest cannot be called from a batch job, a test or another service. The usual result is copying the logic, which then creates duplication and drift. The cure is to extract pure domain logic with explicit inputs, such as a TaxCalculator taking Money, Region and LocalDate.
Is all duplicated code bad?
No. What matters is duplicated knowledge, not duplicated text. If the same business rule, such as free shipping over 50 euros, is written in checkout, UI and invoicing, every change must find all copies, and missed or slightly different copies become bugs, like > in one place and >= in another. But two pieces of code that look identical yet represent different rules, owned by different people and changing for different reasons, should stay separate; merging them creates coupling and an abstraction that bends under both sets of requirements. A good test is to ask whether a change to one would always require the same change to the other. If not, it is coincidence.
How would you measure design quality in a Java codebase?
I combine static metrics with change history, because structure only costs money where code changes. Statically, I look at package dependency cycles with jdeps or ArchUnit, efferent and afferent coupling and instability per package, class cohesion such as LCOM4, cognitive complexity from SonarQube and duplicated blocks from PMD CPD, interpreted with care since each metric has blind spots. Then I add behavioural analysis from Git: hotspots, meaning files that are both complex and frequently changed, and change coupling, meaning files that always change together without an import relationship. Finally I check delivery data such as change failure rate and lead time per area. The output is a short, prioritised list.
How do you stop a design from decaying over time?
Decay happens through many locally reasonable shortcuts, so the defence has to be continuous and mostly automatic. First, encode key structural rules as tests: no package cycles, correct layering and a domain independent of frameworks, using ArchUnit or Spring Modulith verification in CI so violations fail the build. Second, make refactoring part of every feature rather than a separate project, leaving touched code slightly better. Third, review boundaries and dependencies in design reviews and record significant decisions in ADRs. Fourth, track hotspots and change failure rate over time, so emerging problem areas are visible early. Finally, keep the test suite fast, because teams only refactor when they trust the safety net.
How do these concepts apply to a microservices landscape?
The same properties apply, but distribution multiplies their cost. Each service should be highly cohesive around one business capability and loosely coupled to others through stable APIs and events. The symptoms then appear at system scale: rigidity shows up as services that must be changed and deployed in lock-step, fragility as one service's failure or schema change cascading into incidents elsewhere, and immobility as capabilities that a new channel cannot reuse without dragging in another service's data. Duplicated business rules across services cause inconsistent behaviour between channels. Tight coupling between services also costs latency and availability, so drawing boundaries around cohesive capabilities is the architect's main lever.