The Transformer Pattern: Maintaining API Backward Compatibility During Database Refactoring

📖 3 minutes read

Refactoring your database schema but need to keep old API endpoints working? The transformer pattern saved us when we had to evolve our data model without breaking integrations.

The Problem

You’ve built a new, better data structure. But you have external clients consuming your API who expect the old format. Breaking changes mean angry developers and broken integrations.

The naive solution? Keep two codepaths. The smart solution? Use a transformer.

The Pattern


namespace App\Transformers;

class ProductDataTransformer
{
    public static function toLegacyFormat(Product $product): array
    {
        // New structure: flexible, normalized
        $newData = [
            'id' => $product->id,
            'variants' => $product->variants->map(function ($variant) {
                return [
                    'sku' => $variant->sku,
                    'pricing' => $variant->pricingRules->toArray(),
                    'availability' => $variant->availabilitySlots->toArray(),
                ];
            }),
        ];

        // Transform to old structure for backward compatibility
        if ($product->hasFeature('legacy_format')) {
            return self::transformToV1($newData);
        }

        return $newData;
    }

    private static function transformToV1(array $newData): array
    {
        // Old API expected flat structure
        $legacyData = [
            'product_id' => $newData['id'],
            'prices' => [],
            'slots' => [],
        ];

        foreach ($newData['variants'] as $variant) {
            // Flatten pricing rules
            foreach ($variant['pricing'] as $rule) {
                $legacyData['prices'][] = [
                    'sku' => $variant['sku'],
                    'amount' => $rule['base_price'],
                    'currency' => $rule['currency'],
                ];
            }

            // Flatten availability
            foreach ($variant['availability'] as $slot) {
                $legacyData['slots'][] = [
                    'sku' => $variant['sku'],
                    'date' => $slot['start_date'],
                    'available' => $slot['quantity'] > 0,
                ];
            }
        }

        return $legacyData;
    }
}

The Controller


class ProductApiController extends Controller
{
    public function show(Request $request, Product $product)
    {
        // New clients get new format
        if ($request->wantsJson() && $request->header('API-Version') === 'v2') {
            return response()->json($product->toArray());
        }

        // Old clients get transformed legacy format
        return response()->json(
            ProductDataTransformer::toLegacyFormat($product)
        );
    }
}

Feature Flag Integration

The transformer checks a feature flag on the model ($product->hasFeature('legacy_format')) to decide which format to return. This lets you:

  1. Migrate products gradually (not all-at-once)
  2. Test the new format with specific products first
  3. Roll back instantly if something breaks

// In Product model
public function hasFeature(string $feature): bool
{
    return $this->features->contains('name', $feature);
}

// Or simpler: database column
public function hasFeature(string $feature): bool
{
    return (bool) $this->{"use_{$feature}"};
}

Why This Works

The transformer is a translation layer between your modern data model and legacy API contracts. You get:

  • Single source of truth – New data model is the reality, old format is just a view
  • Gradual migration – Feature flags control which products use new vs old format
  • No code duplication – One data model, multiple representations
  • Clear boundaries – Transformation logic is isolated, not scattered across controllers

When to Use This

Apply this pattern when:

  • You have external API clients you can’t coordinate with
  • Breaking changes would cause integration failures
  • You’re refactoring database schema incrementally
  • Different clients need different data formats

Don’t use it for internal refactoring where you control all consumers – just update the code directly.

Real-World Results

We migrated 500+ products from a flat pricing structure to a flexible, variant-based model over 3 months. The transformer kept old integrations working while we gradually moved products to the new format. Zero downtime, zero broken integrations.

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 *