Table of Contents
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:
- Migrate products gradually (not all-at-once)
- Test the new format with specific products first
- 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.
Leave a Reply