ArchitectMind
  1. Home
  2. Tutorials
  3. Book 5 · Software Design & Design Patterns
  4. Volume I
  5. Good vs Poor Design
Book 5 · Volume I — Software Design Foundations & Principles · Topic 1.3

Good vs Poor Design

Coupling, cohesion, complexity, duplication, rigidity, fragility and immobility

10 min readStage 2 · Frameworks & design8 interview questions
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. 1. Concept / Definition
  2. 2. Why It Exists
  3. 3. Core Concepts
  4. 4. How It Works Internally — From Causes to Symptoms
  5. 4A. Deep Dive — Measuring Design Quality
  6. 5. APIs / Features / Syntax — Java Tools Against Poor Design
  7. 6. Practical Examples
  8. 7. Common Problems & Mistakes
  9. 8. Performance & Production Considerations
  10. 9. Architecture & Design Considerations
  11. 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.

Coupling and cohesion together
Figure 1 — Coupling and cohesion together
CategoryItemGoodPoor
StructureCouplingFew, narrow, stable dependenciesMany, broad, volatile dependencies
StructureCohesionEverything in a module serves one purposeUnrelated responsibilities mixed
BurdenComplexityClose to the problem's essential complexityAccidental complexity dominates
BurdenDuplicationEach piece of knowledge in one placeSame rule in several places
SymptomRigidityChanges are localChanges cascade
SymptomFragilityChanges break only what they touchChanges break unrelated places
SymptomImmobilityParts reusable elsewhereParts inseparable from context
Table 1 — Good vs poor design at a glance

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.

JAVA
// 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 couplingWhy it hurts
Reaching through objects (a.getB().getC().doX())Knows structure of distant objects (Law of Demeter)
new of concrete collaboratorsCannot substitute; hard to test
Shared mutable global stateHidden interactions
Boolean/flag parameters steering logicCaller knows callee's internals
Table 2 — Signs of tight coupling (details in Topic 3.3)

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.

JAVA
// 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).

Complexity and duplication accumulate
Figure 2 — Complexity and duplication accumulate
Source of accidental complexityExample
Deep nesting and long methods300-line method with 6 nested ifs
Unnecessary indirectionFactory creating a builder creating a strategy with one implementation
Hidden dependenciesStatic calls, thread-locals, reflection
Inconsistent conventionsThree ways to do the same thing in one codebase
Primitive obsessionString for email, IBAN, currency — rules scattered
Table 3 — Common accidental complexity

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.

JAVA
// 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.

Ripple effect of a change
Figure 3 — Ripple effect of a change
CauseTypical fix
Concrete dependencies everywhereDepend on abstractions (Dependency Inversion)
Decisions not hidden (e.g. data format known by all)Information hiding (Topic 2.2)
Wide interfaces used by manyNarrow, role-specific interfaces
Table 4 — Rigidity causes and fixes

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.

JAVA
// 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 }
CauseTypical fix
Hidden coupling via shared data, magic values, conventionsExplicit types, single definitions
Global mutable stateEncapsulated, injected state
Missing tests at boundariesContract and characterization tests
Table 5 — Fragility causes and fixes

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.

JAVA
// 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;
    }
}
Symptoms of poor design
Figure 4 — Symptoms of poor design

4How It Works Internally — From Causes to Symptoms

CauseLeads to symptom
High couplingRigidity (cascades), fragility (hidden breakages), immobility (can't extract)
Low cohesionRigidity (many reasons to change), fragility (unrelated code co-located)
DuplicationFragility (missed copies), rigidity (many edits)
Accidental complexityAll three; plus slow understanding
Table 6 — How structural problems surface

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

MetricMeasuresToolCaution
Efferent/afferent coupling (Ce/Ca), instabilityCoupling between packagesJDepend-style, Structure101, jdepsInterpret per package role
LCOM4Cohesion of a classIDE plugins, Sonar pluginsRecords/DTOs skew results
Cyclomatic complexityPaths through a methodSonarQube, PMDDoes not capture readability
Cognitive complexityUnderstandabilitySonarQubeHeuristic
Duplicated lines/blocksTextual duplicationSonarQube, PMD CPDMisses conceptual duplication
Dependency cyclesPackage tanglesArchUnit, jdepsZero cycles is achievable
Table 7 — Design metrics

4A.2 Behavioural (change-based) analysis

Static metrics describe structure; version-control history shows how it behaves under change:

AnalysisReveals
Hotspots (change frequency × complexity)Where poor design costs most
Change coupling (files changed together)Hidden coupling not visible in imports
Code age and ownershipKnowledge risks
Table 8 — Evolutionary analysis (e.g. CodeScene-style)
JAVA
// 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

ProblemJava feature / tool
Coupling to concretionsInterfaces, constructor injection, JPMS exports
Low cohesionPackages by feature, small classes, records for data
Duplication of rulesValue objects, enums with behaviour, single policy classes
Magic values (fragility)Enums, sealed types, constants in one place
ComplexityPattern matching switch replacing if-chains, streams for collection logic (judiciously)
CyclesArchUnit beFreeOfCycles(), JPMS (no cyclic requires)
Table 9 — Language and tooling support

6Practical Examples

6.1 Diagnosing a change request

Request: "Add Swiss francs as a supported currency."

ObservationDiagnosis
Currency codes as strings in 40 filesDuplication, primitive obsession
Formatting logic in UI, reports and PDFs separatelyDuplication, low cohesion
Estimate unclear: "somewhere between 3 and 20 days"Rigidity
Last similar change broke invoice PDFsFragility
Table 10 — Symptoms point to causes

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

JAVA
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

JAVA
// 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

QuestionTargets
"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
Table 11 — Questions that expose poor design

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 issueProduction effect
FragilityRegressions after deployments; higher change failure rate
RigiditySlow hotfixes during incidents
DuplicationInconsistent behaviour between channels (web vs batch)
ComplexityLonger incident diagnosis (MTTR)
Table 12 — Poor design shows up in operations

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

Q1BasicConcept

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.

Q2BasicConcept

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.

Q3IntermediateTricky

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.

Q4IntermediateConcept

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.

Q5Senior EngineerTricky

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.

Q6Senior EngineerScenario

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.

Q7Technical ArchitectDesign

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.

Q8Solution ArchitectDesign

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.

Newsletter

Get new tutorials every week

New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.

Almost done! Check your inbox and click the confirmation link to complete your subscription.
Keep going in Book 5

Software Design & Design Patterns

Volume I · Topic 1.1

Software Design

Software Design Foundations & Principles

Volume I · Topic 1.2

Why Software Design Matters

Software Design Foundations & Principles

Volume II · Topic 1.1

Objects

Object-Oriented Design & SOLID

Volume II · Topic 1.2

Classes

Object-Oriented Design & SOLID

Volume II · Topic 1.3

Relationships

Object-Oriented Design & SOLID

Volume III · Topic 1.1

What Is a Design Pattern?

Creational & Structural Design Patterns

Volume III · Topic 1.2

Pattern Categories

Creational & Structural Design Patterns

Volume III · Topic 1.3

Pattern vs Framework

Creational & Structural Design Patterns

Volume IV · Topic 1.1

Strategy Pattern

Behavioral Design Patterns

Volume IV · Topic 1.2

Strategy in Java

Behavioral Design Patterns

Volume IV · Topic 1.3

Strategy vs Conditional Logic

Behavioral Design Patterns

Volume V · Topic 1.1

Layered Architecture

Enterprise & Application Design Patterns

Volume V · Topic 1.2

Service Layer

Enterprise & Application Design Patterns

Volume V · Topic 1.3

Repository

Enterprise & Application Design Patterns

Volume VI · Topic 1.1

What Is Refactoring?

Refactoring, Code Quality & Evolutionary Design

Volume VI · Topic 1.2

Why Refactor?

Refactoring, Code Quality & Evolutionary Design

Volume VI · Topic 1.3

Refactoring Safety

Refactoring, Code Quality & Evolutionary Design

Volume VII · Topic 1.1

Method-Level Design

Advanced Design & Architecture-Level Design Decisions

Volume VII · Topic 1.2

Class-Level Design

Advanced Design & Architecture-Level Design Decisions

Volume VII · Topic 1.3

Module-Level Design

Advanced Design & Architecture-Level Design Decisions