We’ve all been there. You’re building a feature, let’s say a "Data Source Management" tool that helps you manage your data pipelines and you need to track its progress. You create a single field called status. It starts simple: building, ready, or released.
But then, the requirements grow. You need to know if the data from the source is in review or your extraction job for the sources is scheduled to run. You need to know if the data source is obsolete or blocked. Suddenly, your status field is doing the heavy lifting for three different business concepts at once.
While it feels like a shortcut today, mixing different aspects of an object into a single status field is a recipe for a maintenance nightmare.
The Problem: Overloaded Statuses
When we talk about "status", we are often conflating different lifecycles of an object.
Imagine you are managing a data pipeline. You have a Data Source object that reflects a website to be scraped. You need to track whether it's currently extracting data and where it stands in your development pipeline.
The Shortcut: "Status" Junk Drawer
In a rush to ship, you create one status field. Your values look like this:
- building
- ready
- released
- scheduled
- paused
- maintenance
- obsolete
The Crash: One day, a developer needs to put a source into maintenance to fix a bug. They update the status. Because the status is no longer scheduled, the background worker stops pulling data. However, when the fix is done, they set it to ready.
The Disaster: The source is now "Ready," but the data is still missing because the system forgot it was supposed to be scheduled. You’ve overwritten operational intent with development progress.
Why This Leads to Disaster
- Lack of Documentation: While working on a feature, we rarely document that "Status X actually means the user did Y but Z hasn't happened yet." Other developers (or you, six months from now) will have no idea what the side effects of changing a status are.
- State Collisions: You want to move an object to ready state, but doing so accidentally overwrites the scheduled status. Now, the system thinks the source is ready but doesn’t reflect the scheduling of the source.
- The "Boolean Soup": To fix the single-field problem, developers often start adding hidden flags like isProcessingStep2. Without a clear structure, you end up with a "franken-state" that is impossible to debug.
The Geometry of State
When you rely on a single status field, you are forcing multi-dimensional business logic into a 1D line. This is a mathematical trap. Consider a scenario where you have 3 lifecycle stages (e.g., 'Draft', 'Active', 'Archived') and 3 operational states (e.g., 'Running', 'Paused', 'Error').
- In a single 'junk drawer' field, you are forced to manage a Cartesian product of these states, resulting in 9 possible combinations (3x3) that you have to define and handle.
- By decoupling these into independent fields, you only need to manage 6 states (3+3).
In system design, when two pieces of information can change independently of each other, they are 'orthogonal' (as discussed in Joseph Ingeno's Software Architect's Handbook). By splitting these into distinct fields, you are creating Orthogonal States. This strategy effectively prevents the 'state space explosion' that occurs when you try to cram every possible permutation into a single enum.
Before: The 'Junk Drawer' Enum
This approach forces you to manage the Cartesian product of your states (e.g., 3 x 3 = 9 possible combinations):
# Danger: Hard to maintain and prone to collisions
# Representing all combined states as one field
status_options = [
'draft_running', 'draft_paused', 'draft_error',
'active_running', 'active_paused', 'active_error',
'archived_running', 'archived_paused', 'archived_error'
]After: Orthogonal Fields
By separating concerns, you only need to manage the sum of your states (3 + 3 = 6 combinations), and you eliminate invalid states by design:
# Clarity: Independent control over lifecycle and operations
lifecycle_stage = ['draft', 'active', 'archived']
operational_state = ['running', 'paused', 'error']
The Fix: Multi-Dimensional Status Mapping
To avoid this, we must decouple the how (the schedule) from the what (the lifecycle). The fix is simple but requires discipline: One field, one responsibility - a core tenet of the Single Responsibility Principle as defined in Robert C. Martin’s Clean Code and Clean Architecture. Instead of one giant "status" column, break the object into independent lifecycles.
Why this saves your pipeline:
By using distinct fields, you can have a source that is deployment_stage: "maintenance" but still execution_status: "scheduled".
This allows your system to say: "I am currently fixing a bug in the code (Maintenance), but I still want the engine to try and pull data every hour (Scheduled)." If these were the same field, you would be forced to choose one piece of information to lose.
By using different field names for different statuses, you ensure that:
- States are independent: Updating the deployment status doesn't accidentally "pause" a source run.
- Querying is trivial: You can easily find all "Active" sources that are in "Maintenance" without parsing complex strings.
- The code is self-documenting: The field names themselves tell the story of what part of the object they control.
Best Practices for State Architecture
- Identify Independent Lifecycles: Before coding, map out the different "journeys" an object takes. Does it have a lifecycle journey? A logistical journey? A visibility journey (e.g., draft vs. published)? Each gets its own field.
- Enforce Exclusivity: Ensure that values in a single field are truly mutually exclusive. If an object can be both "Flagged" and "In Progress," those are two different fields.
- Use Explicit Naming: Avoid generic names like status or state. Use descriptive prefixes like code_status or scheduled_status.
- Centralise Transitions: Instead of allowing any component to update a status, use a "State Machine" pattern or a centralised service to handle transitions. This prevents an object from jumping from building to maintenance without passing through release.
For comprehensive design principles and guidelines on state architecture, refer to Distributed and Enterprise State Management as a recommended resource.
For managing state transitions, the State pattern described in Design Patterns: Elements of Reusable Object-Oriented Software (1994) provides a robust framework for object behaviour based on internal state changes.
State management isn't just about making the code work today; it’s about ensuring the system remains easy to maintain a year from now. When we overload a single status field, we aren't just saving a few bytes of database space, we are accumulating technical debt with high interest rates.
The Golden Rule: If you find yourself using underscores or hyphens to combine two concepts into one status value (like active_paused), you need a second column. Clean state leads to clean logic, and clean logic leads to a codebase that can actually grow without breaking.
Every time you squeeze a new business rule into an existing status enum, you blur the lines of your domain logic. This ambiguity is where bugs live. By committing to independent status fields, you transform your state from a "black box" of mysterious strings into a clear, multidimensional map of your business reality.
Final Thought: Think in Dimensions
Next time you're tempted to add awaiting_review_v2 to your status enum, stop. Ask yourself: Is this a new stage in the current lifecycle, or a completely different dimension of the object?
If it’s the latter, give it its own field. Your future self, and your maintenance budget, will thank you.
