The Status Field That Grew Three Heads (And How We Fixed It)

Published: (January 14, 2026 at 09:16 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

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

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?

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.

Back to Blog

Related posts

Read more »

A Love-Hate Letter do Json

Using JSON as a configuration format is a mistake. Yes, a mistake. Not a preference. Not a style choice. An architectural mistake. That statement tends to bothe...