DEV Community

Cover image for Ditch Polymorphism: Scaling Laravel with JSONB
Prajapati Paresh
Prajapati Paresh

Posted on • Originally published at smarttechdevs.in

Ditch Polymorphism: Scaling Laravel with JSONB

The Polymorphic Spaghetti Trap

When building enterprise SaaS platforms at Smart Tech Devs, you often need to attach custom metadata or settings to various models. For example, a User, a Company, and an Invoice all need a way to store "Custom Preferences". The standard Laravel reflex is to create a Polymorphic Relationship: a single preferences table with preferenceable_id and preferenceable_type columns.

At a small scale, this is fine. But at enterprise scale, Polymorphic relationships become a severe architectural bottleneck. Querying them requires massive, slow LEFT JOIN operations. If you need to find all Users who have the preference "dark_mode = true", your database has to scan across completely unrelated data (Invoices, Companies) just to filter the Users. Your schema becomes a tangled web of spaghetti, and your read performance plummets. To handle flexible, unstructured data efficiently, you must migrate to PostgreSQL JSONB columns.

The Solution: The JSONB Data Type

PostgreSQL's JSONB (JSON Binary) column allows you to store flexible, schema-less data directly inside the parent table row. It does not require joins. More importantly, PostgreSQL allows you to create GIN Indexes on JSONB columns, meaning querying deep inside the JSON structure is as fast as querying a standard indexed column.

Step 1: Architecting the JSONB Column

We drop the bloated polymorphic tables and add a single, high-performance JSONB column directly to our primary models.


use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddPreferencesToUsersTable extends Migration
{
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            // 1. Add the JSONB column with a default empty object
            $table->jsonb('preferences')->default('{}');
        });

        // 2. ✅ THE ENTERPRISE PATTERN: Add a GIN Index
        // This makes querying specific keys inside the JSON mathematically instant.
        DB::statement('CREATE INDEX users_preferences_gin ON users USING GIN (preferences);');
    }
}

Step 2: Querying JSONB in Eloquent

Laravel provides fluent, native support for querying deep into JSON structures using the -> operator. Because we added a GIN index, this query skips sequential scanning entirely.


namespace App\Http\Controllers\Api;

use App\Models\User;
use Illuminate\Http\Request;

class UserController extends Controller
{
    public function getNightModeUsers()
    {
        // ❌ THE ANTI-PATTERN (Polymorphic):
        // User::whereHas('preferences', fn($q) => $q->where('key', 'theme')->where('value', 'dark'))->get();
        // Result: Massive JOIN overhead, slow execution.

        // ✅ THE ENTERPRISE PATTERN (JSONB):
        // Query directly into the JSON binary structure. No joins required.
        $users = User::where('preferences->theme', 'dark')
            ->where('preferences->notifications->email', true)
            ->get();

        return response()->json($users);
    }
    
    public function updateTheme(Request $request, User $user)
    {
        // Laravel seamlessly updates specific nested keys without overwriting the whole object
        $user->update([
            'preferences->theme' => $request->input('theme')
        ]);
        
        return response()->json(['status' => 'updated']);
    }
}

The Engineering ROI

By migrating flexible metadata from Polymorphic tables into indexed JSONB columns, you drastically simplify your database schema. You completely eliminate the CPU overhead of polymorphic JOIN operations, reduce disk fragmentation, and grant your application NoSQL-like flexibility while maintaining the strict transactional integrity of a relational database.

Top comments (0)