Skip to main content
CleanCodeMastery

Parallel Inheritance Hierarchies: Every Sweet Needs Its Shadow Box

Learn the Parallel Inheritance Hierarchies code smell with a sweet-shop story, mirrored class trees explained simply, TypeScript and C# examples, fixes, and practice.

27 min read Updated June 11, 2026beginner
parallel inheritance hierarchiescode smellschange preventersinheritancemove methodtypescript

🍬 The sweet shop with shadow work

Gupta Sweets in Nagpur is famous for its besan ladoo. The shop is run by Gupta-ji, and his system works — but watch what happens every time he adds a new sweet to the menu.

Last Diwali, Gupta-ji decided to introduce kaju katli. Simple, right? Make the sweet, put it in the display case, done? Not at all. His son Montu pulled out the shop's rule book:

  1. New sweet → we need a new box design. Ladoo goes in round boxes, barfi in flat silver boxes — so kaju katli needs its own special diamond-shaped box. Order it from the printer.
  2. New sweet → we need a new price tag type. Each sweet has its own tag style with its own weight rules. Design one more.
  3. New sweet → we need a new column in the register. The sales register has one column per sweet. Rule the page again, add a column.

So "adding one sweet" is never one job. It is always four jobs: the sweet, the box, the tag, and the column. Montu groaned, "Papa, every sweet comes with three shadows! We never add one thing — we add the thing plus its shadow in the box cupboard, its shadow on the tag board, and its shadow in the register."

Follow Montu through that Diwali week. Making the sweet was the fun part; chasing its shadows ate the rest of the week:

Figure 1: Adding ONE sweet is a week of shadow work — and the forgotten column surfaces only later

And here is the trap: the shadows are connected by memory, not by any rule. Last year, the shop added moong dal halwa but forgot to add the register column. For two weeks, halwa sales were scribbled in the margin, and the monthly accounts did not tally. Nobody broke a rule — there was simply no rule forcing the shadows to stay in step.

That evening Montu asked the question this whole article answers: "Papa, why does the box need to be a separate system at all? The ladoo already knows it is round. Why can the sweet not carry its own box knowledge with it?"

Software does this too. Sometimes a codebase has two class trees that mirror each other. Add a subclass to tree one, and you must add a matching subclass to tree two, or things quietly break. This smell is called Parallel Inheritance Hierarchies — every new class forces you to create its shadow.

🤔 What is this smell?

Parallel Inheritance Hierarchies happens when two class hierarchies are locked together: every time you make a subclass of one class, you also have to make a matching subclass of another.

Martin Fowler lists it among the classic smells in Refactoring, and he points out something very useful: it is really a special case of Shotgun Surgery. Remember Shotgun Surgery? One change, many scattered edits. Here, the "one change" is adding a new variant (a new sweet, a new shape, a new event type), and the "scattered edits" have a very particular structure — they land in two mirrored class trees that must stay synchronized.

Picture it. You have a Sweet hierarchy: Ladoo, Barfi, Jalebi. And somewhere else, a SweetBox hierarchy: LadooBox, BarfiBox, JalebiBox. The prefixes rhyme. The trees have the same shape. They grow at the same speed. A diagram of one is a diagram of the other with the suffix changed. When you add KajuKatli, you must remember to add KajuKatliBox — and "remember" is doing all the work in that sentence.

How does code get into this state? Usually with good intentions. Someone pulled a responsibility — packing, rendering, validating, exporting — out of the main hierarchy into its own tree. That instinct can be healthy! The problem starts when the new tree is forced to mirror the old one one-for-one, subclass by subclass. At that point, the two trees are not independent designs; they are one design split across two places, held together by naming conventions and tribal memory.

💡

Easy memory trick: Parallel hierarchies = every sweet needs its shadow box. If adding ONE new class always forces you to add its twin somewhere else, the two trees are chained. The cure is usually to move the twin's behavior onto the main class and delete the shadow tree.

A note on family relations: this smell belongs to the Change Preventers, alongside Divergent Change and Shotgun Surgery. All three make change expensive — this one makes extension expensive: the most common change of all, "add one more kind of thing," always costs double.

The whole smell, on one revision map:

Figure 2: The Parallel Hierarchies mind map — the rhyming-names sign is the fastest tell

College corner: This smell is a beautiful study in coupling without dependency. KajuKatli and KajuKatliBox may share zero imports — a static analyzer sees two unrelated classes — yet they are tightly coupled, because a change to one requires a change to the other. This is sometimes called implicit or semantic coupling, and it is the most dangerous kind precisely because no tool draws an arrow for it. The general lesson for your design courses: the true coupling graph of a system is the "must change together" graph, and it is always a superset of the import graph.

🔍 How to spot it

This smell is one of the easiest to spot visually, because it announces itself in the class names. The checklist:

  • Two hierarchies whose subclass names rhyme: CircleShapeCircleRenderer, SquareShapeSquareRenderer.
  • Adding a subclass to one tree without its twin causes a compile error, a runtime "no handler found", or a silently missing feature.
  • The two trees always grow together — every PR that adds a class in tree A also adds a class in tree B.
  • Code bridging the trees has a switch/if ladder or a registry: "if it is a Ladoo, use a LadooBox; if a Barfi, use a BarfiBox..."
  • The team has a remembered rule: "whenever you add an XEvent, also add an XEventHandler" — written nowhere, enforced by nobody.

As a quick-reference table:

SignWhat you observeWhat it means
Rhyming namesMatching prefixes across two treesThe trees are one concept split in two
Lockstep growthBoth trees gain subclasses in the same commitsAdding a variant always costs two classes
Bridge ladderswitch/if mapping tree A types to tree B typesDispatch by type is gluing the trees together
Twin-or-breakForgetting the twin breaks compile or runtimeSynchronization is enforced only by memory
Tribal rule"Remember to also add the matching X"The contract between trees is invisible
Same diagramOne tree's diagram = the other's, renamedPure structural duplication

A practical trick: pick the newest subclass in one hierarchy and search your git history for the commit that added it. If that same commit (or a hurried follow-up commit titled "oops, add missing handler") also adds a class in another hierarchy, you have your proof.

You can also simply count the bill for the last variant someone added. Where did the work actually go? In a healthy design, the pie is one slice — the variant itself. In a parallel-hierarchy shop, the shadows eat most of the pie:

Figure 3: The bill for adding kaju katli — the sweet itself is the smallest part of the work

⚠️ Why it is a problem

1. Every extension costs double (or triple). Adding a variant should be the cheapest, most joyful change in object-oriented code — one new class, plugged in. With parallel trees, it is two or three coordinated edits, in different folders, that must match exactly. The most frequent change in the system carries a permanent surcharge.

2. The synchronization has no guard. Nothing in the language enforces "every Sweet must have a SweetBox". The rule lives in convention. Conventions get forgotten — especially by new team members, especially on Friday evenings. That is how Gupta-ji's halwa got no register column.

3. The trees drift. Over the years, one tree gains a subclass the other lacks, or the bridging map misses an entry. The mismatch produces subtle bugs: the new event type that is silently never handled, the new report that exports as an empty file.

4. Double the reading load. A newcomer must understand two hierarchies plus the invisible contract binding them, just to grasp one concept. The mental model is twice the size it needs to be.

5. It is Shotgun Surgery, institutionalised. A single conceptual change — "support hexagons" — becomes a coordinated multi-class operation forever. The smell does not just slow one change; it taxes every future variant, permanently, until someone collapses the mirror.

Figure 4: Two trees in lockstep — adding one variant always forces adding its shadow twin

College corner: Connect this to change amplification (Ousterhout): the idea "we now sell kaju katli" is one sentence, but the implementation is three coordinated edits — the amplification factor is baked into the architecture and charged on every future variant. Also connect it to the Open-Closed Principle: OCP promises that adding a variant should require only adding code, never modifying existing code. Parallel hierarchies break that promise twice — you must modify the bridge ladder, and you must remember to extend a second tree. When a design makes the most frequent change the most error-prone one, the design is optimised for the wrong thing.

💻 A real-life code example

Let us write Gupta Sweets' software, with the smell baked in. There is a Sweet hierarchy, and a parallel PackagingBox hierarchy, plus the tell-tale bridge that maps one to the other.

// Tree 1: the sweets
abstract class Sweet {
  constructor(public name: string, public pricePerKg: number) {}
}
class Ladoo extends Sweet {}
class Barfi extends Sweet {}
class Jalebi extends Sweet {}
 
// Tree 2: the shadow tree — one box class per sweet class
abstract class PackagingBox {
  abstract label(sweet: Sweet, kg: number): string;
}
class LadooBox extends PackagingBox {
  label(s: Sweet, kg: number) { return `ROUND BOX | ${s.name} | ${kg}kg | keep away from sun`; }
}
class BarfiBox extends PackagingBox {
  label(s: Sweet, kg: number) { return `FLAT SILVER BOX | ${s.name} | ${kg}kg | refrigerate`; }
}
class JalebiBox extends PackagingBox {
  label(s: Sweet, kg: number) { return `LEAK-PROOF BOX | ${s.name} | ${kg}kg | best fresh`; }
}
 
// The bridge: the glue that proves the trees are chained
function boxFor(sweet: Sweet): PackagingBox {
  if (sweet instanceof Ladoo)  return new LadooBox();
  if (sweet instanceof Barfi)  return new BarfiBox();
  if (sweet instanceof Jalebi) return new JalebiBox();
  throw new Error(`No box registered for ${sweet.name}`); // the halwa trap!
}

The structure, drawn as a class diagram, makes the mirror impossible to miss — fold the page down the middle and the two halves line up name for name:

Figure 5: The mirror in UML — every sweet has a rhyming twin, and the bridge function glues the trees together

Now Diwali comes, and Montu adds kaju katli:

class KajuKatli extends Sweet {}
// Montu compiles, ships, and goes home happy...

The code compiles perfectly. TypeScript raises no objection. But the first time a customer buys kaju katli, boxFor() throws: "No box registered for Kaju Katli." Watch the failure arrive in slow motion — note especially when each actor finds out:

Figure 6: The forgotten twin fails at the worst possible time — at the billing counter, not at compile time

To add ONE sweet, Montu actually needed THREE edits: the KajuKatli class, a KajuKatliBox class, and a new branch in boxFor. He made one of three. The other two were shadows, and shadows are exactly what people forget. The compiler — the one friend who could have warned him — stayed silent, because nothing in the type system expresses the rule "every Sweet needs a Box".

Count the cost of every future sweet: two classes plus one bridge branch, forever. And notice what each box subclass really contains — just a box style and a storage note. Tiny data, wearing a heavy class costume.

🛠️ Cleaning it up, step by step

The standard cure, straight from Fowler: make one hierarchy refer to the other, then use Move Method and Move Field to migrate everything onto the primary tree. When the mirror subclasses become empty shells, delete the entire shadow hierarchy.

Step 1 — Identify which tree is primary. Usually it is the one representing the real concept (the Sweet); the shadow exists only to serve it (the Box). The shadow is the one whose subclasses would be meaningless alone — a LadooBox without ladoos serves nobody.

Step 2 — Move the behavior onto the primary tree. Each box subclass holds packaging knowledge about one sweet. That knowledge varies sweet by sweet — which is exactly what polymorphism is for. Use Move Method to bring it home:

abstract class Sweet {
  constructor(public name: string, public pricePerKg: number) {}
  abstract boxLabel(kg: number): string;   // packaging now lives WITH the sweet
}
 
class Ladoo extends Sweet {
  boxLabel(kg: number) { return `ROUND BOX | ${this.name} | ${kg}kg | keep away from sun`; }
}
class Barfi extends Sweet {
  boxLabel(kg: number) { return `FLAT SILVER BOX | ${this.name} | ${kg}kg | refrigerate`; }
}
class Jalebi extends Sweet {
  boxLabel(kg: number) { return `LEAK-PROOF BOX | ${this.name} | ${kg}kg | best fresh`; }
}

Step 3 — Move any data the behavior needs. If LadooBox had fields (box dimensions, material), use Move Field to carry them into Ladoo alongside the method that uses them.

Step 4 — Delete the shadow tree and the bridge. The PackagingBox hierarchy is now empty shells; boxFor() with its instanceof ladder serves no one. Delete both. Less code, fewer files, zero invisible contracts.

Step 5 — Enjoy the new cost of extension. Watch what adding kaju katli costs now:

class KajuKatli extends Sweet {
  boxLabel(kg: number) { return `DIAMOND BOX | ${this.name} | ${kg}kg | refrigerate`; }
}
// Done. One class. No shadow. No bridge edit. Nothing to forget.

And here is the best part: it is now impossible to forget the packaging. boxLabel is abstract on Sweet, so TypeScript refuses to compile a sweet without it. The invisible convention became a compiler-enforced contract. Montu's "oops" bug cannot exist anymore — the whole bug species is extinct.

The full life of the design, from clean to mirrored to collapsed, as a state machine:

Figure 7: The life cycle of the mirror — either collapse it onto the primary tree, or guard it deliberately

And the payoff in plain numbers — the cost of the shop's most common change, before and after:

Figure 8: Edits needed to add one new sweet — three coordinated edits collapse into one class

An alternative when the variation is only data. Look again at those box subclasses — each was just two strings (box style, storage note). When mirror subclasses contain only data, you do not even need methods on each subclass; a simple field does the job. In Python this collapse is especially satisfying:

# The whole shadow hierarchy becomes... constructor data.
from dataclasses import dataclass
 
@dataclass
class Sweet:
    name: str
    price_per_kg: int
    box_style: str       # was a LadooBox / BarfiBox class
    storage_note: str    # was a method override
 
    def box_label(self, kg: float) -> str:
        return f"{self.box_style} | {self.name} | {kg}kg | {self.storage_note}"
 
ladoo = Sweet("Ladoo", 520, "ROUND BOX", "keep away from sun")
kaju_katli = Sweet("Kaju Katli", 980, "DIAMOND BOX", "refrigerate")
# Adding a sweet is now ONE line. No class, no twin, no bridge.

Whole hierarchies collapse into constructor arguments. Always check whether your shadow tree is really behavior (keep polymorphism) or just data (use fields).

⚠️

Sometimes the shadow tree exists for a good reason — for example, a Renderer hierarchy kept separate so that UI code never enters your pure domain model. Merging would drag a UI framework into the model: a worse problem! In that case, keep the trees but manage the mirror deliberately, using a registry or the Visitor pattern instead of scattered instanceof ladders. A chosen, guarded parallelism is a trade-off; an accidental one is a smell.

🧪 The same smell in C#

A compact C# version, from a reporting system. Every report type has a shadow exporter type:

// Before: two trees in lockstep + a bridge switch
public abstract class Report { }
public class SalesReport : Report { }
public class StockReport : Report { }
 
public abstract class ReportExporter { public abstract string Export(Report r); }
public class SalesReportExporter : ReportExporter
{
    public override string Export(Report r) => "sales as CSV...";
}
public class StockReportExporter : ReportExporter
{
    public override string Export(Report r) => "stock as CSV...";
}
 
public static ReportExporter ExporterFor(Report r) => r switch
{
    SalesReport => new SalesReportExporter(),
    StockReport => new StockReportExporter(),
    _ => throw new InvalidOperationException("No exporter!") // the trap
};

Adding a TaxReport requires three synchronized edits. Collapse the mirror by moving the export behavior onto the report:

// After: one tree; the shadow hierarchy and the switch are deleted
public abstract class Report { public abstract string Export(); }
public class SalesReport : Report { public override string Export() => "sales as CSV..."; }
public class StockReport : Report { public override string Export() => "stock as CSV..."; }
public class TaxReport   : Report { public override string Export() => "tax as CSV..."; }
// One new report = one new class. The compiler enforces Export(). Nothing to forget.

If exporting must stay out of the domain (a fair architectural choice), then keep the separation but make the link explicit and checked — a registry dictionary that fails fast at startup is far safer than a switch that fails at 6 p.m. on Diwali.

⚖️ Divergent Change vs Shotgun Surgery — and where this smell sits

Every Change Preventer student must carry this map in their head. The two parent smells are mirror opposites, and Parallel Inheritance Hierarchies has a precise seat at the table: it is Shotgun Surgery with structure.

QuestionDivergent ChangeShotgun SurgeryParallel Hierarchies
Shape of the painONE class, MANY reasonsONE reason, MANY classesONE new variant, TWO (or more) mirrored trees
Story versionOne clerk, every department disturbs himOne address change, ten officesEvery new sweet needs its shadow box
The cure directionSplit the classGather the scattered piecesGather — fold the mirror tree into the main one
Main refactoringsExtract Class, Move MethodMove Method, Move Field, Inline ClassMove Method, Move Field, then delete the shadow tree
Which side of the see-sawThe "too gathered" sideThe "too scattered" sideThe scattered side, in uniform
Figure 9: The Change Preventers map — parallel hierarchies are a structured case of the scatter side

The two diagnostic questions still work perfectly here:

  1. For one kind of change (adding a variant), how many classes do I edit? Two or three, always — so this is on the Shotgun Surgery side, and the instinct is to gather.
  2. Is one class being hit by many unrelated reasons? No — so this is not Divergent Change, and splitting would be the wrong medicine entirely.

On the diagnosis map, the sweet shop sits in the Shotgun Surgery corner — one reason (a new sweet), several classes touched — just closer to the centre than a free-form scatter, because the scatter here is disciplined and predictable:

Figure 10: Plotting the sweet shop — structured scatter still lands on the Shotgun Surgery side of the map

That second diagnostic point matters. If you misdiagnosed parallel hierarchies as "too much in one place" and split further, you would create a third mirrored tree — three shadows per sweet instead of two! Remember the rule: split for Divergent Change, gather for Shotgun Surgery — and parallel hierarchies always fall on the gather side.

College corner: Why does the scatter side feel so familiar here? Because the mirror is a cohesion failure dressed as a coupling decision. The knowledge "how a ladoo is packed" is cohesive with the ladoo — they belong in one module — yet it was placed in a different tree, creating cross-tree coupling that no import statement records. When you study coupling metrics, remember that the worst offenders rarely show up in dependency graphs: lockstep co-change between Ladoo and LadooBox is invisible to the compiler and obvious only in the commit history. High cohesion is not just an aesthetic; it is what makes the "must change together" graph and the module graph agree.

🕵️ Where this smell hides in real projects

The catalogues (Fowler's Refactoring, refactoring.guru, sourcemaking, and the community smell catalogues) point to recurring habitats:

  • Event systems. An OrderPlacedEvent needs an OrderPlacedEventHandler; a PaymentFailedEvent needs a PaymentFailedEventHandler. The handler tree faithfully shadows the event tree. (Often acceptable — but watch for a third tree appearing: validators, serializers...)
  • Model + renderer/view pairs. CircleShape/CircleRenderer, PdfBlock/PdfBlockPainter. Born from the healthy wish to keep drawing code out of geometry — turned smelly when the trees lock one-for-one.
  • Entity + DTO + mapper triplets. Every Customer entity has a CustomerDto and a CustomerMapper; every new entity births the whole family. Some layering is deliberate; three rhyming trees growing in lockstep deserve at least a code generator or mapping library.
  • Test class shadows. Some teams mandate exactly one test class per production class, mirrored by name. The mirror is mostly harmless, but it can push people to test structure instead of behavior.
  • Plugin and serializer registries. A ThingX plus a ThingXSerializer plus a registry line. Frameworks often soften this with reflection or attributes — which is really the framework managing the parallel hierarchy for you.

The common thread: the second tree usually began as a good separation of concerns. The smell is not the separation itself — it is the one-for-one lockstep plus the invisible contract holding it together.

😌 When it is okay to ignore

SituationIgnore it?Why
Merging would drag an unwanted dependency into the model (UI into domain)Yes — keep the treesThe separation is buying real architectural cleanliness; manage the mirror with a registry or Visitor
Both trees are tiny and have not gained a subclass in yearsYesLockstep maintenance of two stable classes costs almost nothing
A framework or code generator maintains the twin automaticallyMostlyThe synchronization has a guard; the human-memory danger is gone
Only some subclasses need twins, and missing twins fail fast at startupOftenA partial, checked mapping is a managed design, not an accident
New variants are added every month and twins get forgottenNo — fix itThe smell is actively producing "no handler found" bugs
The mirror has already drifted (orphan subclasses, stale bridge map)No — fix itDrift is the disease in progress; collapse or guard the mirror now

The honest summary: this smell has the strongest legitimate exceptions in the Change Preventers family, because separating concerns into a second hierarchy is sometimes excellent architecture. Judge by two tests — does the separation buy real value? and is the synchronization guarded by anything stronger than memory? If yes and yes, keep it. If no and no, collapse it.

💊 Which refactorings cure it

RefactoringWhat it does hereWhen to reach for it
Move MethodMoves each twin's behavior onto the corresponding primary subclassThe main collapsing tool — polymorphism replaces the mirror
Move FieldCarries the twin's data along with its behaviorWhen mirror subclasses hold state, not just methods
Inline ClassFolds an emptied twin into its primary classThe cleanup step once a twin has nothing left
Replace Subclass with FieldsTurns data-only twins into simple constructor dataWhen the shadow tree varies only by values, not behavior
Visitor pattern (a design pattern, not a refactoring)Manages a deliberate mirror safelyWhen the trees must stay separate for architectural reasons

The procedure in one breath: make an instance of one hierarchy hold or resolve its twin, move the differentiating methods and fields across with Move Method and Move Field, watch the twin subclasses empty out, then delete the whole shadow tree. Two trees become one, and adding a variant becomes a single joyful class.

📦 Quick revision box

+--------------------------------------------------------------+
|      PARALLEL INHERITANCE HIERARCHIES — QUICK REVISION       |
+--------------------------------------------------------------+
| Story    : Every new sweet forces a new box design, a new    |
|            price tag type, and a new register column         |
| Smell    : Add a subclass in tree A -> MUST add its twin     |
|            in tree B (names rhyme: XShape <-> XRenderer)     |
| Family   : Change Preventers — special case of               |
|            Shotgun Surgery (structured scatter)              |
| Spot it  : Rhyming prefixes; lockstep growth; bridge         |
|            switch ladders; "remember the twin" tribal rule   |
| Costs    : Double cost per variant; unguarded sync; drift;   |
|            double reading load; forgotten-twin bugs          |
| Cure     : GATHER -> Move Method + Move Field onto the       |
|            primary tree; delete the empty shadow tree        |
| Keep it  : Only when separation buys real architecture —     |
|            then guard it (registry / Visitor), don't trust   |
|            memory                                            |
| Memory   : One thing should never need a shadow -> MERGE     |
+--------------------------------------------------------------+

✏️ Practice exercise

Time to free a shop from its shadows!

Exercise 1 — Spot the mirror. A delivery app has these classes: BikeDelivery, VanDelivery, DroneDelivery — and elsewhere: BikeFareCalculator, VanFareCalculator, DroneFareCalculator, plus this bridge:

function calculatorFor(d: Delivery): FareCalculator {
  if (d instanceof BikeDelivery)  return new BikeFareCalculator();
  if (d instanceof VanDelivery)   return new VanFareCalculator();
  if (d instanceof DroneDelivery) return new DroneFareCalculator();
  throw new Error("No calculator found");
}

List: (a) the two hierarchies, (b) the invisible contract, (c) exactly what breaks — and when it breaks — if someone adds CycleDelivery but forgets its twin.

Exercise 2 — Collapse it. Refactor so each delivery class has its own fare(distanceKm: number) method. Delete the calculator hierarchy and the bridge. How many edits does adding CycleDelivery need now? What stops you from forgetting the fare logic?

Exercise 3 — Data or behavior? Suppose every fare was just baseCharge + perKm * distance with different numbers per vehicle. Show how the whole calculator hierarchy could become two constructor fields instead of subclass methods (like the Python Sweet dataclass above). Which solution do you prefer here, and why?

Exercise 4 — Draw your own Figure 8. Take any project that has rhyming class trees (events and handlers count!). Count the edits required to add one new variant today, and the edits after a collapse. Sketch the before/after bar chart. If the bar does not shrink, your mirror may be a deliberate boundary — apply the two tests from the "when to ignore" section.

Exercise 5 — The judgement call. A game keeps Monster subclasses in the core engine and MonsterSprite subclasses in the graphics module, mirrored one-for-one. The team lead refuses to merge them: "graphics code must never enter the engine." Is the parallelism acceptable here? If you keep it, name one mechanism that protects the team from forgotten twins better than human memory.

(Hints: Exercise 1c — nothing breaks at compile time; the error explodes at runtime, on the first cycle delivery booking. Exercise 5 — yes, this is a legitimate architectural separation; protect it with a startup-time registry check that fails fast if any Monster lacks a sprite, or manage the mirror with the Visitor pattern.)

You have now met all three Change Preventers. Revise the family in one line: split the overloaded clerk (Divergent Change), gather the scattered offices (Shotgun Surgery), and merge the shadow trees (this one). Make changes cheap, and every other improvement becomes possible.

Frequently asked questions

What is the Parallel Inheritance Hierarchies smell in one line?
It is when two class trees are locked in lockstep: every time you add a subclass to one tree, you are forced to add a matching 'twin' subclass to the other tree. Adding one thing always forces adding its shadow.
How do I recognise parallel hierarchies quickly?
Look at the class name prefixes. If CircleShape has a CircleRenderer, SquareShape has a SquareRenderer, and TriangleShape has a TriangleRenderer, the prefixes rhyme across two trees — that mirrored naming is the classic signature.
How is this smell related to Shotgun Surgery?
It is a structural special case of Shotgun Surgery. One conceptual change — adding a new variant — forces coordinated edits in multiple places, here in two mirrored class trees. The cure is the same gathering instinct: fold the mirror tree into the main one.
Which refactorings cure parallel hierarchies?
Move Method and Move Field. Make one hierarchy refer to the other, then move the differing behavior and data onto the primary type. When the mirror subclasses become empty, delete the whole second tree.
Is it ever okay to keep two parallel hierarchies?
Yes, when merging would pull an unwanted dependency into your model — for example, UI rendering code into pure domain classes. In that case the parallelism is a deliberate trade-off, and patterns like Visitor or a strategy registry can manage the mirror more safely than scattered switches.

Further reading

Related Lessons