Separate Display Names from System Identifiers

📖 2 minutes read

When building systems with both user interfaces and internal logic, explicitly separating display names from system identifiers prevents cascading changes when marketing decides to rename a feature.

The pattern: use stable identifiers (type, id, slug) for code/database/filenames, and store display names (name, label) separately in configuration:

// config/features.php
return [
    'available_reports' => [
        [
            'type' => 'sales_report',               // Stable system identifier
            'name' => 'Quarterly Sales Analysis',   // User-facing display name
            'permission' => 'view_sales_reports',
        ],
        [
            'type' => 'inventory_report',
            'name' => 'Stock Level Summary',
            'permission' => 'view_inventory',
        ],
    ],
];

In your controllers:

class ReportController
{
    public function index()
    {
        $reports = collect(config('features.available_reports'))
            ->filter(fn ($report) => auth()->user()->can($report['permission'] ?? ''))
            ->map(fn ($report) => [
                'id' => $report['type'],      // Internal ID for API/routes
                'label' => $report['name'],   // Display name for UI
            ]);
            
        return view('reports.index', compact('reports'));
    }
    
    public function generate(string $reportType)
    {
        // Use 'type' for routing, job dispatch, filename generation
        $job = match ($reportType) {
            'sales_report' => new GenerateSalesReport(),
            'inventory_report' => new GenerateInventoryReport(),
            default => throw new InvalidArgumentException(),
        };
        
        dispatch($job);
    }
}

Job classes use the stable identifier:

class GenerateSalesReport implements ShouldQueue
{
    public function handle()
    {
        $filename = 'sales_report_' . now()->format('Y-m-d') . '.pdf';
        
        Storage::put("exports/{$filename}", $this->generatePdf());
        
        // Filename: 'sales_report_2024-03-05.pdf' — never changes
    }
}

Why this matters:

  • Marketing freedom: “Quarterly Sales Analysis” can become “Revenue Insights Dashboard” without touching code
  • Stability: Database queries, API endpoints, and filenames don’t break when display names change
  • A/B testing: Easily test different labels for the same feature
  • Internationalization: Display names can be translated while system identifiers stay English

What NOT to do:

// ❌ DON'T couple display names to class constants
class GenerateSalesReport
{
    public const DISPLAY_NAME = 'Quarterly Sales Analysis';
    
    public function getFilename()
    {
        return self::DISPLAY_NAME . '_' . now()->format('Y-m-d') . '.pdf';
        // Filename: 'Quarterly Sales Analysis_2024-03-05.pdf' — spaces, changes when label changes
    }
}

// ❌ DON'T hardcode display names in multiple places
// Controller
$reportName = 'Quarterly Sales Analysis';

// Blade view

Quarterly Sales Analysis

// Email notification Mail::send(..., ['report' => 'Quarterly Sales Analysis']); // Now you have 3+ places to update when marketing changes the name

The right approach:

  • Store display names in config/*.php or database tables where non-developers can update them
  • Use system identifiers everywhere in code (sales_report, not "Quarterly Sales Analysis")
  • Fetch display names at runtime from the centralized source

Your codebase becomes more flexible, and non-technical stakeholders can update user-facing labels without opening a pull request.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *