Why Eloquent Models Are Not Your Database Rows: Hydration, Dirty Tracking, and the Identity Map
It's easy to think of an Eloquent model as just an object wrapper around a database row: fetch a row, get an object, call save(), row updates. That mental model works fine right up until you hit a bug that only makes sense once you understand what's actually happening between the query and the object in your hand. This is a look under the hood at hydration, dirty tracking, and why two "identical" models in the same request aren't always the same thing.
Hydration: turning rows into objects
When Eloquent runs a query, the database driver returns plain arrays, one per row. Hydration is the process of turning each array into a model instance:
$users = User::where('active', true)->get();
Under the hood, this isn't "create a User, then assign properties." Eloquent's newFromBuilder() method builds the model differently from new User(): it sets the raw attributes directly and marks the model as exists = true, skipping mutators and casts during the initial fill so hydration stays fast even for large result sets. Casts and accessors only run lazily, when you actually access an attribute.
This is why a raw DB::table()->get() and an Eloquent::get() on the same table return meaningfully different things, even though both technically ran "the same query." One gives you stdClass rows; the other gives you objects with relationships, casts, mutators, and change tracking layered on top.
Dirty tracking: how save() knows what changed
When a model is hydrated, Eloquent stores a copy of the original attributes alongside the current ones. Every time you set a property, Eloquent doesn't immediately compare it to the database, it compares it to that original snapshot:
$user = User::find(1);
$user->isDirty(); // false
$user->email = 'new@phparchitect.com';
$user->isDirty(); // true
$user->isDirty('name'); // false, only email changed
$user->getDirty(); // ['email' => 'new@phparchitect.com']
When you call save(), Eloquent only writes the columns that are actually dirty, not the entire attribute set. This is why you can safely load a model, change one field, and save it, without accidentally overwriting concurrent changes to other columns made by a different process, as long as that process also only touched the columns it cared about.
It also explains a common surprise: if you fetch a model, mutate an array-cast attribute in place rather than reassigning it, dirty tracking can miss the change:
$user->preferences['theme'] = 'dark'; // may not mark as dirty depending on cast
$user->preferences = $user->preferences; // reassigning forces the dirty check to see it
Understanding dirty tracking as "diffed against the original hydrated snapshot," not "diffed against the live database," makes this behavior predictable instead of mysterious.
The identity map problem Eloquent doesn't solve
Some ORMs guarantee that fetching the same row twice in one request returns the exact same object instance, an "identity map." Eloquent doesn't do this by default:
$a = User::find(1);
$b = User::find(1);
$a === $b; // false, two separate objects, even though same row
Each query hydrates a brand-new object. If you mutate $a->name without saving, $b->name won't reflect it, because they aren't the same object, they just happen to represent the same row at the moment each was fetched. This matters most when a single request loads the same model through two different code paths, say, once through a relationship and once through a direct query, and you expect a change made through one to be visible through the other. It won't be, until both are reloaded from the database or explicitly kept in sync.
Why this mental model matters
None of this is really about knowing Eloquent's internals for their own sake. It's about recognizing three classes of bugs before they cost you an afternoon: assuming save() writes every attribute (it doesn't, only dirty ones), assuming array-cast mutations are automatically tracked (they often aren't), and assuming two fetches of "the same" row give you one shared object you can mutate from either reference (they don't). Once you see Eloquent as "a snapshot with a diff mechanism," rather than "a live view of the database row," these stop being surprises and start being predictable.