Gilded Rose Kata: What Everyone Misses
The Setup
The Gilded Rose Refactoring Kata by Terry Hughes and Emily Bache is one of the most popular refactoring exercises. It presents messy, nested conditional code that needs cleaning up.
Everyone focuses on eliminating the nested if statements. And that's good! But they all miss something deeper.
What Most Solutions Look Like
The Strategy Pattern (Most Common)
interface UpdateStrategy {
void update(Item item);
}
class BackstagePassStrategy implements UpdateStrategy {
public void update(Item item) {
if (item.sellIn <= 0) {
item.quality = 0;
} else if (item.sellIn <= 5) {
item.quality = Math.min(50, item.quality + 3);
} else if (item.sellIn <= 10) {
item.quality = Math.min(50, item.quality + 2);
} else {
item.quality = Math.min(50, item.quality + 1);
}
item.sellIn--;
}
}
This is clean. This is maintainable. For the kata, this is probably the best solution.
The Polymorphic Approach (Most Elegant)
class BackstagePass extends Item {
void updateQuality() {
sellIn--;
if (sellIn < 0) {
quality = 0;
} else if (sellIn <= 5) {
quality = Math.min(50, quality + 3);
} else if (sellIn <= 10) {
quality = Math.min(50, quality + 2);
} else {
quality = Math.min(50, quality + 1);
}
}
}
This is beautiful. It mirrors the domain. Sandi Metz's celebrated solution uses this approach.
Both solutions are simpler than what I'm about to show you.
The Question Nobody Asks
Look at those conditionals again:
if (sellIn <= 5) quality += 3;
else if (sellIn <= 10) quality += 2;
else quality += 1;
Now answer these questions:
- What are the distinct phases in a backstage pass's lifecycle?
- When exactly does it transition from one phase to another?
- How would you add a new phase (+4/day when sellIn ≤ 2)?
You can figure out the answers by reading the code carefully. But they're not explicit. They're implicit in the conditional logic.
What Everyone Misses: The Temporal State Machine
The Gilded Rose isn't really about item types (Normal, Brie, Pass).
It's about how items change over time.
The Hidden Pattern
Backstage Pass Lifecycle:
┌─────────────────┐
│ Far Future │ sellIn > 10 → quality +1/day
│ (>10 days) │
└────────┬────────┘
│ sellIn ≤ 10
↓
┌─────────────────┐
│ Near Event │ 6 ≤ sellIn ≤ 10 → quality +2/day
│ (6-10 days) │
└────────┬────────┘
│ sellIn ≤ 5
↓
┌─────────────────┐
│ Very Close │ 1 ≤ sellIn ≤ 5 → quality +3/day
│ (1-5 days) │
└────────┬────────┘
│ sellIn < 0
↓
┌─────────────────┐
│ Expired │ quality = 0 (terminal)
└─────────────────┘
These are STATES. The backstage pass transitions through them over time.
Normal Item Lifecycle
Normal Item Lifecycle:
┌─────────────────┐
│ Fresh │ sellIn ≥ 0 → quality -1/day
└────────┬────────┘
│ sellIn < 0
↓
┌─────────────────┐
│ Expired │ sellIn < 0 → quality -2/day
└────────┬────────┘
│ quality ≤ 0
↓
┌─────────────────┐
│ Worthless │ quality = 0 (terminal)
└─────────────────┘
Strategy Pattern solves: "Different item TYPES need different logic"
Polymorphic Pattern solves: "Item TYPES are distinct domain entities"
State Machine reveals: "Items have TEMPORAL LIFECYCLES with distinct phases"
Why This Matters
1. State Transitions Are Now Explicit
Before (Strategy Pattern):
if (item.sellIn <= 10) {
// What state is this? When did we enter it?
item.quality += 2;
}
After (State Machine):
if (item.sellIn <= 10) {
return new BackstagePassNearEvent(); // EXPLICIT transition
}
You can see when and why state changes happen.
Critical Detail: Order of Operations Matters
When implementing state transitions, the order you check conditions is crucial:
// ❌ WRONG ORDER - Can cause bugs
public ItemState tick(Item item) {
item.sellIn--;
// Check transition BEFORE degrading
if (item.sellIn < 0 && rate == 1) {
return new DegradingState(2);
}
item.quality = Math.max(0, item.quality - rate);
// Problem: We transition based on OLD sellIn value
}
// ✅ CORRECT ORDER
public ItemState tick(Item item) {
item.sellIn--;
// Apply quality change FIRST
item.quality = Math.max(0, item.quality - rate);
// Check terminal state
if (item.quality == 0) {
return TerminalState.INSTANCE;
}
// Then check for state transition
// Use <= because we already decremented sellIn
if (item.sellIn <= 0 && rate == 1) {
return new DegradingState(2);
}
return this;
}
Why this matters:
- Apply effects first - Quality should change in current state
- Check terminal conditions - Quality = 0 takes priority
- Then check transitions - Only transition if not terminal
- Use correct threshold - After decrementing, check
sellIn <= 0not< 0
This ordering encodes a business rule: "An item degrades in its current state, then transitions if needed."
State machines make this explicit. In scattered conditionals, you'd never notice the bug.
2. Adding New Behavior Is Surgical
Requirement: "Add +4/day when sellIn ≤ 2"
Strategy Pattern:
// Modify existing method, insert new condition
if (item.sellIn <= 2) {
item.quality = Math.min(50, item.quality + 4);
} else if (item.sellIn <= 5) {
item.quality = Math.min(50, item.quality + 3);
}
// Risk: off-by-one errors, order matters
State Machine:
// Add new state class
class BackstagePassCritical implements ItemState {
public ItemState tick(Item item) {
item.quality = Math.min(50, item.quality + 4);
if (item.sellIn <= 1) return new BackstagePassFinal();
return this;
}
}
// Update VeryClose to transition to it
if (item.sellIn <= 2) return new BackstagePassCritical();
The new phase is a first-class entity, not a conditional branch.
3. Testing Becomes About States, Not Scenarios
Strategy Pattern Tests:
@Test void backstagePass_with_8_days_remaining() { /* ... */ }
@Test void backstagePass_with_5_days_remaining() { /* ... */ }
@Test void backstagePass_with_0_days_remaining() { /* ... */ }
You're testing specific scenarios.
State Machine Tests:
@Test void backstagePass_transitions_through_urgency_levels() {
assertEquals(BackstagePassFarFuture, pass.state);
// Tick until transition
pass.tickUntil(sellIn == 10);
assertEquals(BackstagePassNearEvent, pass.state);
pass.tickUntil(sellIn == 5);
assertEquals(BackstagePassVeryClose, pass.state);
pass.tickUntil(sellIn < 0);
assertEquals(BackstagePassExpired, pass.state);
}
You're testing state transitions and temporal progression.
The Core Insight
What You Thought You Were Solving
"How do I eliminate these nested conditionals?"
What You Were Actually Solving
"How do I model temporal state transitions with phase-dependent behavior?"
The nested conditionals were a SYMPTOM.
The missing state model was the DISEASE.
When This Pattern Appears in Real Code
This isn't just academic. This pattern is everywhere:
Order Processing
Everyone writes:
if (order.status == PAID && order.shippingLabel != null) {
// Ship it
}
Nobody writes:
order.state.ship(); // Explicit state transition
// Can only ship if in "ReadyToShip" state
User Onboarding
Everyone writes:
if (user.emailVerified && user.profileComplete && !user.sawWelcome) {
showWelcome(user);
}
Nobody writes:
user.onboardingState.completeStep();
// Explicit progression through onboarding phases
Subscription Management
Everyone writes:
if (subscription.trialEnded() && !subscription.hasPaidPlan()) {
convertToFree(subscription);
}
Nobody writes:
subscription.state.tick();
// Trial → Active → PastDue → Canceled
// Explicit lifecycle
The Thought Process: How to Recognize This Pattern
Question 1: Does behavior change over time?
Gilded Rose: Yes! Items age, passes get more valuable as events approach.
If yes: Consider temporal states.
Question 2: Are there distinct phases?
Gilded Rose: Yes! Fresh vs Expired, Far vs Near vs Very Close.
If yes: Model them explicitly.
Question 3: Do phases have clear transition points?
Gilded Rose: Yes! sellIn thresholds (10, 5, 0).
If yes: State machines make transitions first-class.
Question 4: Does adding a new phase touch multiple places?
Gilded Rose: With conditionals, yes. With states, no.
If yes: States isolate change.
Comparing Approaches
| Aspect | Strategy | Polymorphic | State Machine |
|---|---|---|---|
| Simplicity for kata | ✓ Best | ✓ Best | △ More complex |
| Explicit states | ❌ No | ❌ No | ✓ Yes |
| Explicit transitions | ❌ No | ❌ No | ✓ Yes |
| Temporal model visible | ❌ No | ❌ No | ✓ Yes |
| Class count | 5 | 5 | 5-10 |
| Learning value | ✓ Good | ✓ Excellent | ✓ Different insight |
The Meta-Lesson
Good refactoring eliminates complexity.
Great refactoring reveals the hidden domain model.
The Gilded Rose isn't teaching you to eliminate nested if statements.
It's teaching you to recognize when temporal state transitions are the underlying pattern.
When to Use Each Approach
Use Strategy/Polymorphic When:
- You have < 10-15 item types
- Behavior differences are primarily by TYPE
- Temporal phases are simple (< 3 states)
- Team is unfamiliar with state machines
- For the Gilded Rose kata itself
Use State Machines When:
- Behavior changes significantly over TIME
- Multiple types share the same lifecycle
- You have 4+ distinct temporal phases
- Phase transitions have complex rules
- Debugging requires "what state am I in?"
- For real systems that scale beyond toy problems
The Honest Answer for Gilded Rose
For the kata itself: Use Strategy Pattern or Polymorphic Inheritance.
For learning: Understand that temporal state transitions are the hidden pattern.
For your production code: Recognize when this pattern applies and choose the appropriate abstraction.
What Makes the State Machine Different
It's not about being "better." It's about seeing a different dimension of the problem.
Strategy/Polymorphic see: Item TYPES (what kind of thing) State Machine sees: Item STATES (what phase of lifecycle)
Both are valid. Both are valuable. They're orthogonal concerns.
In complex systems, you might even use both:
abstract class Item {
ItemState state;
abstract ItemState initialState();
void tick() {
state = state.tick(this);
}
}
class BackstagePass extends Item {
ItemState initialState() {
return sellIn <= 5 ? new VeryClose() :
sellIn <= 10 ? new NearEvent() :
new FarFuture();
}
}
Type hierarchy (Sandi Metz's insight) + State machine (temporal insight) = Best of both worlds.
The Challenge to You
Go back to your own codebase. Look for this pattern:
if (condition_based_on_time_or_progression) {
// Do something
} else if (another_temporal_condition) {
// Do something else
}
Ask yourself:
- Are there distinct phases hidden in these conditions?
- Would making them explicit help?
- Do multiple entities share this lifecycle?
If the answer is yes, you've found a state machine waiting to be modeled.
Further Learning
This is one of 20+ complexity patterns we teach in our course on managing software complexity.
The pattern here is Specification-Driven Case Splits solved with Finite State Machines (from our Case Splits framework).
Want to learn all 20+ patterns systematically? Learn more about the course
Credits
- Original Kata: Terry Hughes (2011)
- Kata Repository: Emily Bache
- Polymorphic Approach: Sandi Metz - "All the Little Things"
- State Machine Analysis: This article + The Repo
Summary
What everyone solves: Nested conditionals
What everyone misses: Temporal state transitions
The lesson: Some problems aren't about types—they're about how things change over time.
The skill: Recognizing when temporal lifecycle modeling is the underlying pattern.
That's what the Gilded Rose kata is really teaching you.