Same Record, Different Instance: A Subtle Eloquent Bug

A subtle Eloquent bug where the same database record is represented by different model instances with different in-memory state.

While working on a delivery order processing flow, I encountered an issue where two consecutive operations produced an unexpected quantity.

The flow itself was straightforward:

public function handle(SalesOrder $sales_order): void
{
    $this->processingService->attachToDeliveryOrder(
        $sales_order,
        $delivery_order
    );

    // another process

    $this->processingService->completeDeliveryOrder(
        $sales_order,
        $delivery_order
    );
}

The first operation prepares the remaining sales order quantity for a delivery order. The second operation completes the delivery and moves that prepared quantity into the processed quantity.

At first glance, both operations are working with the same SalesOrder.

The problem was that they weren’t necessarily working with the same Eloquent model instances.

Querying a Relationship Can Create a Second Instance

Inside attachToDeliveryOrder(), the sales order details were retrieved through the relationship query:

protected function attachToDeliveryOrder(
    SalesOrder $sales_order,
    DeliveryOrder $delivery_order
): void {
    $sales_order_details = $sales_order->sales_order_details()
        ->get();

    foreach ($sales_order_details as $detail) {
        $detail->increment(
            'prepared_do_quantity',
            $remaining_quantity
        );
    }
}

Later, completeDeliveryOrder() accessed the already-loaded relationship:

protected function completeDeliveryOrder(
    SalesOrder $sales_order,
    DeliveryOrder $delivery_order
): void {
    foreach ($sales_order->sales_order_details as $detail) {
        $detail->increment(
            'processed_do_quantity',
            $detail->prepared_do_quantity
        );
    }
}

These two expressions look similar:

$sales_order->sales_order_details()
$sales_order->sales_order_details

But they have an important difference.

The first returns a relationship query. Calling get() executes another database query and hydrates the results into new Eloquent model instances.

The second accesses the relationship collection already loaded on $sales_order.

As a result, the application could end up with two objects representing the exact same database row.

One Database Row, Two PHP Objects

The execution flow effectively looked like this:

                 SalesOrderDetail #123

            ┌────────────┴────────────┐
            │                         │
    Loaded Instance            New Instance
            │                         │
            │               attachToDeliveryOrder()
            │                         │
            │                prepared += quantity


completeDeliveryOrder()

            └── reads the old in-memory state

Both instances have the same primary key and represent the same database record, but they are different PHP objects.

This can easily be overlooked during debugging because:

$loaded_detail->id === $queried_detail->id;
// true

while:

$loaded_detail === $queried_detail;
// false

Updating one instance does not synchronize every other instance of that model currently living in memory.

For example, assume the database row starts with a prepared quantity of 0, and the remaining quantity is 5:

// New instance retrieved by attachToDeliveryOrder()
$queried_detail->increment('prepared_do_quantity', 5);

// The database value is now 5, but this is a different instance.
$loaded_detail->prepared_do_quantity;
// 0

$loaded_detail->increment(
    'processed_do_quantity',
    $loaded_detail->prepared_do_quantity
);
// processed_do_quantity += 0

The second operation reads the stale value from $loaded_detail, even though the database already contains the updated prepared quantity.

That distinction was the root cause of the incorrect quantity calculation.

Reuse the Loaded Relationship Collection

In this particular flow, I didn’t need another representation of the sales order details. I needed to continue working with the instances already attached to the SalesOrder.

Instead of querying the relationship again:

$sales_order_details = $sales_order->sales_order_details()->get();

I reused the already-loaded relationship collection:

$sales_order_details = $sales_order->sales_order_details;

Now both operations work with the same model instances throughout the process:

              SalesOrderDetail #123

                  Instance A

            ┌─────────┴─────────┐
            │                   │
attachToDeliveryOrder   completeDeliveryOrder
            │                   │
    prepared += qty      processed += prepared
            │                   │
            └─────────┬─────────┘

                Same Instance

Why Object Identity Matters

The interesting part of this bug wasn’t increment(), the relationship itself, or even the quantity calculation.

It was object identity.

In Eloquent, the same database record can be represented by multiple model instances during a single request. Querying a relationship again does not guarantee that you’ll continue working with an instance that already exists elsewhere in your application flow.

This distinction matters when multiple operations depend on in-memory changes made earlier in the same process.

A useful mental model is:

Same primary key means the same database record. It does not mean the same PHP object.

Relationship queries and loaded relationships are therefore not always interchangeable:

$model->relation() // relationship query
$model->relation   // loaded relationship value

Most of the time, that distinction is harmless. But when several services mutate the same models sequentially, it can become a surprisingly subtle source of bugs.