I shipped a "simple" status field six months ago.
Yesterday I deleted it and replaced it with... a status field.
Here's how three fields became one, and why it took 193 files to fix a problem I created.
The Original Sin
When I first built import/export configs, the status was obvious:
type ImportConfig struct {
Status string `json:"status"` // "draft" | "active" | "paused"
}
Clean. Simple. Done.
Then users started running imports. I needed to know if the last run succeeded or failed:
type ImportConfig struct {
Status string `json:"status"` // "draft" | "active" | "paused"
LastRunStatus string `json:"lastRunStatus"` // "success" | "failed" | "running"
}
Still manageable. Two fields, two concerns.
Then I built the wizard. Users needed to save progress mid-flow without creating broken configs. Easy fix:
type ImportConfig struct {
Status string `json:"status"`
LastRunStatus string `json:"lastRunStatus"`
IsDraft bool `json:"isDraft"` // wizard in progress
}
Three heads. One monster.
The Combinatorial Nightmare
Here's the problem with three fields: they create a state matrix.
Status: draft | active | paused | disabled | completed
LastRunStatus: success | failed | running | (empty)
IsDraft: true | false
That's 5 × 4 × 2 = 40 possible states.
Most of them were nonsense. What does status=active, lastRunStatus=running, isDraft=true mean?
I had no idea. Neither did my code.
The frontend had this gem:
function getDisplayStatus(config: ImportConfig): string {
if (config.isDraft) return 'draft';
if (config.lastRunStatus === 'running') return 'running';
if (config.status === 'paused') return 'paused';
if (config.lastRunStatus === 'failed') return 'failed';
if (config.status === 'active') return 'ready';
return 'draft'; // ¯\_(ツ)_/¯
}
That function was wrong. I just didn't know which cases were wrong.
The Recognition Moment
I was adding a new feature when I realized I couldn't answer a basic question:
"Is this import ready to run?"
The answer required checking three fields, understanding their precedence, and hoping I got the logic right.
That's when I knew: I hadn't modeled status. I'd accumulated symptoms.
The Fix: One Field to Rule Them All
The refactor was simple in concept:
Before: 3 fields tracking overlapping concerns
After: 1 field with 5 mutually exclusive states
const (
StatusDraft = "draft" // Wizard incomplete, missing required fields
StatusReady = "ready" // Complete config, can be run
StatusRunning = "running" // Currently executing
StatusPaused = "paused" // User paused execution
StatusFailed = "failed" // Last run failed, needs attention
)
type ImportConfig struct {
Status string `json:"status"` // One of the above. That's it.
}
No matrix. No precedence. No guessing.
"Is this import ready to run?" → config.Status == StatusReady
But Wait, What About the Wizard?
The isDraft field existed because wizards need to save partial progress.
Removing it meant solving a different problem: where does wizard state live?
Answer: in a DraftData field that only exists when Status == StatusDraft:
type ImportConfig struct {
Status string `json:"status"`
DraftData *DraftData `json:"draftData,omitempty"` // nil unless Status=draft
}
type DraftData struct {
CurrentStep string `json:"currentStep"`
PendingSchema *PendingSchema `json:"pendingSchema,omitempty"`
PendingDataType *PendingDataType `json:"pendingDataType,omitempty"`
}
The key insight: pending resources live in draft data, not in real tables.
When you're mid-wizard creating a schema, that schema doesn't exist yet. It's a pending schema stored in DraftData. Only when you finalize does it become real.
No more orphaned schemas from abandoned wizards.
The Damage Report
Fixing this touched:
- 193 files
- 11,786 additions
- 4,179 deletions
- Backend models, services, handlers
- Frontend types, hooks, wizards
- Tests across both
Was it worth it?
The getDisplayStatus function is now:
function getDisplayStatus(config: ImportConfig): string {
return config.status;
}
Yes. It was worth it.
Lessons
1. State fields multiply.
One becomes two becomes three. Each addition feels small. The complexity is combinatorial.
2. "What state is this?" should be trivial to answer.
If you need a flowchart, you have a modeling problem.
3. Pending resources aren't real resources.
Wizard state is draft data, not partially-created entities.
4. Big refactors are just many small changes.
193 files sounds scary. It was really "update Status constant" × 193.
Building Flywheel - data pipelines for startups. If you've ever watched a "simple" field grow three heads, I'd love to hear your war stories.

Top comments (0)