Active Record is arguably the most popular ORM pattern in web development. Laravel's Eloquent, Rails' ActiveRecord, Django's ORM - they all follow the same idea. And for simple applications, it works beautifully. The problems start when complexity grows.
The Active Record Pattern
In Active Record, a single class is responsible for:
- Representing a row in the database
- Data validation
- Business logic
- Persistence (save, delete, find)
- Querying other records
This is a lot of responsibilities for one class. Here's a typical Eloquent model:
class Order extends Model
{
protected $fillable = ['customer_id', 'status', 'total'];
public function customer() {
return $this->belongsTo(Customer::class);
}
public function items() {
return $this->hasMany(OrderItem::class);
}
public function ship(): void
{
if ($this->status !== 'paid') {
throw new Exception('Cannot ship unpaid order');
}
$this->status = 'shipped';
$this->shipped_at = now();
$this->save();
// Send notification
Mail::to($this->customer->email)
->send(new OrderShipped($this));
}
public function calculateTotal(): Money
{
return $this->items->sum(fn($item) =>
$item->price * $item->quantity
);
}
} Problem 1: Anemic or Bloated Models
Active Record forces a choice: either your models become bloated with business logic, or you extract that logic elsewhere and your models become anemic (just getters/setters).
Bloated models are hard to test because they depend on the database. Anemic models scatter business logic across services, jobs, and controllers.
Problem 2: The Database is Always Present
You can't create an Order without thinking about the database:
// This hits the database
$order = new Order(['customer_id' => 1]);
$order->save();
// So does this
$order = Order::create(['customer_id' => 1]);
// And testing requires database setup
public function test_order_can_be_shipped()
{
$order = Order::factory()->create(['status' => 'paid']);
$order->ship();
// Requires database, mail fake, etc.
} Problem 3: No Aggregate Boundaries
In a real domain, an Order and its Items should be modified together as a unit (an Aggregate). With Active Record, nothing prevents this:
// Anyone can modify items directly
$item = OrderItem::find(123);
$item->quantity = 999;
$item->save();
// Order total is now wrong! There's no way to enforce that Order must recalculate its total when items change.
Problem 4: Implicit Dependencies Everywhere
Active Record models can query anything:
class Order extends Model
{
public function ship(): void
{
// Why does Order know about Warehouse?
$warehouse = Warehouse::findNearest($this->shipping_address);
// And Shipping rates?
$rate = ShippingRate::calculate($warehouse, $this);
// And inventory?
Inventory::reserve($this->items);
}
} These implicit dependencies make the code hard to understand and test.
The Alternative: Domain Models + Repositories
Separate the domain logic from persistence:
// Pure domain object - no database awareness
final class Order
{
private array $items = [];
private OrderStatus $status;
public function addItem(Product $product, int $quantity): void
{
$this->items[] = new OrderItem($product, $quantity);
$this->recalculateTotal();
}
public function ship(): void
{
if (!$this->status->canTransitionTo(OrderStatus::Shipped)) {
throw new CannotShipOrderException($this->status);
}
$this->status = OrderStatus::Shipped;
$this->recordEvent(new OrderWasShipped($this->id));
}
}
// Repository handles persistence
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
}
// Test without database
public function test_order_can_be_shipped(): void
{
$order = new Order(/*...*/);
$order->markAsPaid();
$order->ship();
$this->assertEquals(OrderStatus::Shipped, $order->status());
} When Active Record is Fine
Active Record works well for:
- Simple CRUD applications
- Prototypes and MVPs
- Admin panels and back-office tools
- Read-heavy operations (use it for queries!)
The pattern breaks down when:
- Business logic is complex
- You need to enforce invariants
- Multiple entities must change together
- You want fast, isolated unit tests
Ready to move beyond Active Record?
Pragmatic DDD with Laravel shows you how to use both patterns: Active Record for simple queries, Domain Models for complex business logic. Get the best of both worlds.
Get the book