2 minute read

Feature Delivery in Flow State

Core Principle

Features are released in small, reversible steps behind feature flags. Always ask: “Can we roll this back?”

Never ship completed features as one big deployment. Break everything into incremental steps that can be independently rolled back.

Laravel Example: Adding due_date Column

Let’s add a due date field to tasks, step by step:

Step 1: Database (Non-destructive)

// Migration - always additive
Schema::table('tasks', function (Blueprint $table) {
    $table->timestamp('due_date')->nullable();
});

Deploy immediately - adding nullable columns is safe.

Step 2: Update Models (Write-only)

class Task extends Model
{
    protected $fillable = ['title', 'description', 'due_date'];
    
    // Don't add due_date to accessors/relationships yet
    // Just allow writing, no reading
}

Why this order: Models can write to new column without breaking existing code.

Step 3: Start Writing (Feature Flag)

class TaskController extends Controller 
{
    public function store(Request $request)
    {
        $data = $request->validated();
        
        if (Feature::active('task_due_dates')) {
            $data['due_date'] = $request->input('due_date');
        }
        
        return Task::create($data);
    }
}

Step 4: Backfill Existing Data (If Needed)

// Artisan command - run when ready
Task::whereNull('due_date')
    ->update(['due_date' => now()->addWeek()]);

Step 5: Start Reading (Feature Flag)

class TaskResource extends JsonResource
{
    public function toArray($request)
    {
        $data = [
            'id' => $this->id,
            'title' => $this->title,
            'description' => $this->description,
        ];
        
        if (Feature::active('task_due_dates')) {
            $data['due_date'] = $this->due_date;
        }
        
        return $data;
    }
}

Step 6: Add Validation (Feature Flag)

class StoreTaskRequest extends FormRequest
{
    public function rules()
    {
        $rules = [
            'title' => 'required|string',
            'description' => 'required|string',
        ];
        
        if (Feature::active('task_due_dates')) {
            $rules['due_date'] = 'required|date|after:now';
        }
        
        return $rules;
    }
}

Step 7: Gradual Rollout

  • Enable for internal users first
  • Monitor error rates and performance
  • Gradually increase percentage: 5% → 25% → 50% → 100%
  • Roll back instantly if issues appear

Step 8: Cleanup

// Remove feature flags once stable
class TaskResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'title' => $this->title, 
            'description' => $this->description,
            'due_date' => $this->due_date, // No flag needed
        ];
    }
}

Key Patterns

Non-destructive changes first: Add columns, don’t modify or remove them.

Write before read: Populate new data before consuming it.

Feature flags everywhere: Every user-facing change should be toggleable(?).

Monitor each step: Watch error rates, performance, user behavior.

Small batches: Each deployment should change one thing.

Rollback Strategy

Each step can be independently rolled back:

  • Database: Nullable columns can be ignored
  • Models: Remove from fillable array
  • Controllers: Toggle feature flag off
  • Resources: Feature flag hides new fields
  • Validation: Feature flag disables new rules

Success Metrics

  • Deploy daily (or more)
  • Rollback in under 5 minutes
  • Less than 2% of deployments require rollback
  • No “big bang” releases

Categories:

Updated: