Gilded Rose: Four Complexity Patterns in One Kata

The Challenge

The Gilded Rose Refactoring Kata is famous for its nested conditional nightmare. Everyone focuses on eliminating the if statements.

But what if the kata is teaching something deeper?

What if it's a perfect sandbox for practicing four fundamental complexity management patterns?


The Four Patterns

1. Boolean Blindness - When Conditions Lie

Look at typical solutions:

if (item.sellIn <= 5) {
    item.quality += 3;
} else if (item.sellIn <= 10) {
    item.quality += 2;
} else {
    item.quality += 1;
}

Questions:

  • What does 5 mean?
  • What state is sellIn <= 10?
  • Why 3 vs 2 vs 1?

The blindness: Booleans and magic numbers hide meaning.

2. Case Splits - When Branches Multiply

if (item.name.equals("Aged Brie")) {
    // Brie logic
} else if (item.name.equals("Backstage passes")) {
    // Pass logic
    if (item.sellIn <= 5) {
        // Nested logic
    }
} else {
    // Normal logic
}

The problem: N item types × M temporal phases = exponential complexity

3. Design Reflectivity - When Code Doesn't Mirror Domain

// Strategy is external to Item
UpdateStrategy strategy = StrategyFactory.forItem(item);
strategy.update(item);

Question: Is the update strategy part of an item's identity, or external to it?

In the domain: An aged brie IS aging. A backstage pass HAS urgency phases.

In the code: Items delegate to external strategies.

The gap: Code structure doesn't reflect domain structure.

4. Method Call Protocol - When Mutation Creates Bugs

item.sellIn = -1;  // Oops, forgot to update state
item.quality = 200; // Oops, violated bounds

The problem: Mutable state allows invalid operations.


The Solution: Immutable Strategy Pattern

Let's address all four patterns simultaneously:

Pattern 1 Solution: Sealed Types (Boolean Blindness)

Instead of:

if (item.sellIn <= 5) { /* mysterious condition */ }

Use explicit types:

sealed interface ItemType permits Normal, AgedBrie, BackstagePass, Legendary, Conjured {
    Item update(Item item);
}

final class BackstagePass implements ItemType {
    private final int qualityIncrease; // Explicit, not magic

    BackstagePass(int increase) {
        this.qualityIncrease = increase;
    }
}

Benefit: BackstagePass(3) tells you MORE than sellIn <= 5

Pattern 2 Solution: Polymorphism (Case Splits)

Instead of:

if (type == NORMAL) { /* ... */ }
else if (type == BRIE) { /* ... */ }

Use polymorphic dispatch:

sealed interface ItemType {
    Item update(Item item); // Each type implements its own logic
}

final class Normal implements ItemType {
    public Item update(Item item) {
        int newSellIn = item.sellIn() - 1;
        int degradation = (newSellIn < 0) ? 2 : 1;
        int newQuality = Math.max(0, item.quality() - degradation);
        return new Item(item.name(), newSellIn, newQuality, this);
    }
}

Benefit: Add new type = add new class. No touching existing code.

Pattern 3 Solution: Composition (Design Reflectivity)

Instead of:

// External strategy
StrategyFactory.forItem(item).update(item);

Make type part of Item's identity:

record Item(String name, int sellIn, int quality, ItemType type) {
    public Item tick() {
        return type.update(this); // Type is PART OF item
    }
}

Benefit: Code mirrors domain. An item HAS-A type, not uses-a strategy.

Pattern 4 Solution: Immutability (Method Call Protocol)

Instead of:

void update(Item item) {
    item.sellIn--;  // Mutation
    item.quality--; // More mutation
}

Return new instances:

public Item update(Item item) {
    int newSellIn = item.sellIn() - 1;
    int newQuality = Math.max(0, item.quality() - 1);
    return new Item(item.name(), newSellIn, newQuality, this);
}

Benefit: Can't violate invariants. Old item is unchanged, new item is validated.


The Complete Implementation

// Pattern 1: Sealed types eliminate boolean blindness
sealed interface ItemType permits Normal, AgedBrie, BackstagePass, Legendary, Conjured {
    Item update(Item item);
}

// Pattern 2: Polymorphism eliminates case splits
final class BackstagePass implements ItemType {
    private final int qualityIncrease;

    BackstagePass(int increase) {
        this.qualityIncrease = increase;
    }

    public Item update(Item item) {
        int newSellIn = item.sellIn() - 1;

        // After concert: worthless
        if (newSellIn < 0) {
            return new Item(item.name(), newSellIn, 0, this);
        }

        // Increase quality
        int newQuality = Math.min(50, item.quality() + qualityIncrease);

        // Transition to higher urgency if needed
        ItemType nextType = this;
        if (newSellIn <= 5 && qualityIncrease < 3) {
            nextType = new BackstagePass(3);
        } else if (newSellIn <= 10 && qualityIncrease < 2) {
            nextType = new BackstagePass(2);
        }

        // Pattern 4: Return new item (immutability)
        return new Item(item.name(), newSellIn, newQuality, nextType);
    }
}

// Pattern 3: Type is part of Item's identity (design reflectivity)
record Item(String name, int sellIn, int quality, ItemType type) {
    public Item tick() {
        return type.update(this);
    }

    // Factory methods hide complexity
    public static Item backstagePass(String name, int sellIn, int quality) {
        return new Item(name, sellIn, quality, new BackstagePass(1));
    }
}

What Makes This Different

vs. Traditional Strategy Pattern

Traditional:

class BackstagePassStrategy implements UpdateStrategy {
    public void update(Item item) {
        item.sellIn--;  // Mutates
        if (item.sellIn <= 5) item.quality += 3; // Magic numbers
    }
}

// External to item
strategy.update(item);

Immutable:

class BackstagePass implements ItemType {
    private final int qualityIncrease; // Explicit

    public Item update(Item item) {
        return new Item(...); // Returns new
    }
}

// Part of item
item = item.tick();

vs. Sandi Metz's Polymorphism

Sandi Metz:

class BackstagePass extends Item {
    void updateQuality() {
        sellIn--;  // Mutates this
        if (sellIn <= 5) quality += 3; // Magic numbers
    }
}

Immutable:

// Item doesn't need subclassing
record Item(String name, int sellIn, int quality, ItemType type) {
    public Item tick() { return type.update(this); }
}

// Type is separate concern
class BackstagePass implements ItemType {
    private final int qualityIncrease; // No magic
    public Item update(Item item) { return new Item(...); }
}

Key difference: Item is a data record, behavior is in types. Follows composition over inheritance.

vs. State Machine

State Machine:

interface ItemState {
    ItemState tick(Item item); // Returns new state, mutates item
}

item.sellIn--;  // Mutation
item.quality += 2; // Mutation
return new BackstagePassVeryClose(); // New state

Immutable:

interface ItemType {
    Item update(Item item); // Returns new item AND new type
}

// Everything immutable
return new Item(name, newSellIn, newQuality, nextType);

Key difference: State machine changes state, keeps item. Immutable changes both.


The Unique Benefits

1. Thread Safety by Default

Mutable:

// Thread 1
item.updateQuality();

// Thread 2
item.updateQuality();

// Race condition! What's the final state?

Immutable:

// Thread 1
Item item1 = item.tick();

// Thread 2
Item item2 = item.tick();

// No race! Each thread has its own Item

2. Time Travel Debugging

Mutable:

item.tick();
item.tick();
// What was item's state 2 ticks ago? Lost forever.

Immutable:

Item day0 = item;
Item day1 = day0.tick();
Item day2 = day1.tick();

// Can still inspect day0, day1
// Perfect for debugging or undo/redo

3. Referential Transparency

Mutable:

Item item = new Item("Bread", 5, 10);
item.tick();
item.tick();
// What's item's quality? Depends on history. Hard to reason about.

Immutable:

Item item = Item.normal("Bread", 5, 10);
Item result = item.tick().tick();
// result is ALWAYS the same for same inputs
// Easy to reason about, easy to test

4. Type Transitions Visible

Mutable:

Item pass = backstagePass(8, 20);
pass.tick();
// Did the type change? Hidden internal state.

Immutable:

Item pass = Item.backstagePass("Concert", 8, 20);
Item newPass = pass.tick();

pass.type();    // BackstagePass(increase=2)
newPass.type(); // BackstagePass(increase=3) - VISIBLE change!

When to Use This Approach

Use Immutable Strategy When:

Thread safety matters

  • Concurrent access to items
  • Parallel processing

Audit trails needed

  • Need to track history
  • Undo/redo functionality
  • Event sourcing

Teaching complexity patterns

  • Boolean blindness
  • Design reflectivity
  • Immutability benefits

Functional programming preferred

  • Team comfortable with FP
  • Referential transparency valued

Don't Use When:

Performance critical

  • GC pressure from many objects
  • Tight loops

Team unfamiliar with FP

  • Immutability is foreign
  • Records/sealed interfaces new

Simple CRUD apps

  • Mutability is fine
  • Overhead not worth it

The Four Patterns in Action

Let's trace one backstage pass through its lifecycle:

// Day 0: Far from concert
Item pass = Item.backstagePass("Concert", 12, 10);
// Type: BackstagePass(increase=1)
// Quality: 10

// Day 1
pass = pass.tick();
// Type: BackstagePass(increase=1) - still far
// Quality: 11 (+1)

// Day 2: Crosses threshold
pass = pass.tick();
// Type: BackstagePass(increase=2) - NOW near! (Pattern 1: explicit type change)
// Quality: 12 (+1, transition happens for NEXT tick)

// Day 3-7: Near event
for (int i = 0; i < 5; i++) {
    pass = pass.tick();
}
// Type: BackstagePass(increase=2)
// Quality: 22 (12 + 2*5)

// Day 8: Very close
pass = pass.tick();
// Type: BackstagePass(increase=3) - urgent! (Pattern 1: type encodes state)
// Quality: 25 (+3)

// Days 9-12
for (int i = 0; i < 4; i++) {
    pass = pass.tick();
}
// Quality: 37 (25 + 3*4)

// Day 13: Concert passes
pass = pass.tick();
// Type: BackstagePass(increase=3) - same type
// Quality: 0 (worthless after concert)

Pattern 1 (Boolean Blindness): Type changes explicitly (increase=1increase=2increase=3)
Pattern 2 (Case Splits): No conditionals needed - polymorphism handles it
Pattern 3 (Design Reflectivity): pass.tick() - the pass itself knows how to age
Pattern 4 (Immutability): Each day is a different Item object


Comparison Summary

Aspect Traditional Sandi Metz State Machine Immutable
Boolean Blindness ❌ Magic numbers ❌ Conditionals △ Some ✅ Sealed types
Case Splits ✅ Strategy ✅ Inheritance ✅ States ✅ Sealed
Design Reflectivity △ External ✅ IS-A △ HAS-A ✅ HAS-A
Immutability ❌ Mutable ❌ Mutable ❌ Mutable ✅ Records
Thread Safety ❌ No ❌ No ❌ No ✅ Yes
Simplicity ✅✅✅ ✅✅✅✅✅ ✅✅ ✅✅✅
Learning Value ✅✅✅ ✅✅✅✅ ✅✅✅✅✅ ✅✅✅✅✅

The Meta-Lesson

The Gilded Rose kata isn't just about refactoring conditionals.

It's a playground for complexity management patterns:

  1. Boolean Blindness - Name your concepts
  2. Case Splits - Use polymorphism
  3. Design Reflectivity - Mirror the domain
  4. Immutability - Prevent invalid states

Traditional solutions teach one or two of these.
Immutable strategy teaches all four.


Course Connection

This is just one example from our Managing Software Complexity course.

We teach 20+ complexity patterns:

  • Boolean blindness
  • Stringly-typed code
  • Method call protocols
  • Case splits
  • Temporal state machines
  • And 15 more...

Each pattern has:

  • How to recognize it
  • Multiple solution strategies
  • Trade-off analysis
  • Real-world examples

Want to master all 20+ patterns? [Learn more about the course]


Summary

For the Gilded Rose kata:

  • Traditional Strategy: Simple, effective ⭐⭐⭐⭐⭐
  • Sandi Metz: Elegant, domain-aligned ⭐⭐⭐⭐⭐
  • State Machine: Temporal patterns ⭐⭐⭐⭐
  • Immutable Strategy: Complexity patterns ⭐⭐⭐⭐⭐

For learning:

  • Each approach teaches different lessons
  • Immutable teaches the most patterns
  • No single "best" solution

For production:

  • Choose based on your constraints
  • Thread safety? → Immutable
  • Simplicity? → Traditional or Sandi Metz
  • Temporal complexity? → State Machine
  • Learning tool? → Immutable

The real insight:

"The best programmers don't just solve problems—they recognize patterns and choose the right abstraction for the context."

The Gilded Rose teaches you to see four different patterns in one problem.

That's the skill that separates good developers from great ones.


Try It Yourself

The Challenge

Implement the Gilded Rose kata using the immutable strategy approach.

Focus on:

  1. Eliminating boolean blindness - Use sealed interfaces and explicit types
  2. Avoiding case splits - Use polymorphism
  3. Reflecting the domain - Make type part of Item's identity
  4. Enforcing immutability - Return new instances

Questions to Consider

  1. Where do you still see magic numbers or booleans?
  2. Are your types closed (sealed) or open?
  3. Does your code structure mirror the domain?
  4. Can you violate invariants through mutation?

The Litmus Test

Good refactoring: Eliminates nested conditionals
Great refactoring: Reveals the domain model
Exceptional refactoring: Teaches multiple complexity patterns

Which level is your solution?


Further Reading

On Boolean Blindness

  • "Parse, Don't Validate" by Alexis King
  • "Making Illegal States Unrepresentable" by Yaron Minsky

On Immutability

  • "Functional Programming in Java" by Venkat Subramaniam
  • "Effective Java" by Joshua Bloch (Item 17: Minimize Mutability)

On Design Patterns

  • "Refactoring" by Martin Fowler
  • "All the Little Things" by Sandi Metz (RailsConf 2014)

On Complexity Management

  • "A Philosophy of Software Design" by John Ousterhout
  • "Domain-Driven Design" by Eric Evans

Credits and Attribution

Original Kata:

Inspiration:

Complexity Patterns:

  • Our Software Craftsmanship course module

Appendix: Complete Code

The complete runnable implementation is available at our git repo

Key features:

  • ✅ Sealed interfaces (Java 17+)
  • ✅ Records for immutability
  • ✅ Factory methods for clean API
  • ✅ Comprehensive test suite
  • ✅ Zero mutations
  • ✅ Thread-safe by default
Day 0:
Normal Item              sellIn:  10, quality:  20 [Normal]
Aged Brie                sellIn:   5, quality:  10 [AgedBrie]
Sulfuras                 sellIn:   0, quality:  80 [Legendary]
Backstage passes         sellIn:  15, quality:  20 [BackstagePass]
Conjured Mana Cake       sellIn:   3, quality:   6 [Conjured]

Day 1:
Normal Item              sellIn:   9, quality:  19 [Normal]
Aged Brie                sellIn:   4, quality:  11 [AgedBrie]
Sulfuras                 sellIn:   0, quality:  80 [Legendary]
Backstage passes         sellIn:  14, quality:  21 [BackstagePass]
Conjured Mana Cake       sellIn:   2, quality:   4 [Conjured]
...

Discussion Questions

For Your Team

  1. Which pattern resonates most? Boolean blindness? Immutability? Design reflectivity?
  2. Where in your codebase do you see these patterns?
    • Magic booleans/numbers?
    • Scattered case statements?
    • Code structure misaligned with domain?
    • Mutation causing bugs?
  3. What's your team's comfort level with:
    • Immutability?
    • Sealed interfaces?
    • Functional programming concepts?
  4. What constraints matter in your context?
    • Thread safety?
    • Performance?
    • GC pressure?
    • Team experience?

For Yourself

  1. Before reading this: What was your approach to the kata?
  2. What surprised you? Which pattern was new?
  3. What will you apply? Which pattern helps your current project?
  4. What's still unclear? Where do you need more examples?

Engage with Us

Found this valuable?

  • ⭐ Star the repository
  • 💬 Leave a comment with your approach
  • 🔄 Share with colleagues
  • 📧 Subscribe for more complexity patterns

Disagree with something?

Great! Let's discuss:

  • What would you do differently?
  • Which pattern don't you find valuable?
  • What's your context that makes this not work?

Want to learn more?

The Gilded Rose reveals four patterns. Our course teaches 20+:

  • Stringly-typed code
  • Linguistic antipatterns
  • Method call protocols
  • Representational independence
  • MIRO state modeling
  • Causal dependencies
  • And 14 more...

Learn about the full course →


Final Thought

"The Gilded Rose kata is a gift. It's simple enough to understand in minutes, yet rich enough to teach dozens of patterns.

Most people use it to practice refactoring.

The best developers use it to learn how to see complexity patterns.

Which will you be?"

Thank you for reading!

If this helped you see complexity patterns in a new light, share it with someone who might benefit.

And remember: The code you write today teaches the patterns you'll recognize tomorrow.

Master the patterns. Solve any problem.

Read more