Distributed Business Logic with Laravel Observers: Let Models Mind Their Own Business
Writing business logic in a controller or service is easy, but just because it is easy doesn’t mean it’s clean.If your models are dumb and all the brain lives in controllers, you should reconsider your approach.Imagine a scenario where an order is placed in an e-commerce system. You need to do the following.
- Place the order
- Calculate costing, pricing and discounts before saving
- Generate order number before saving
- Send emails to customer and store admin
- Produce notification sound in admin panel to get attention
This is a typical example where you need to do multiple steps to complete a process. Unless you do all these steps, the operation is considered incomplete. But to achieve this, many people use a single class and cram all the logic into it to execute this whole process. This pollutes the class with much code. Instead we can take advantage of Laravel Observers to place the logic where it belongs.Let’s explore how we can take advantage of Laravel Observers, a powerful, underrated pattern in Laravel, and write distributed business logic.
1. Save Data
Let’s say a user is placing an order and we need to save it in the database. The most useful approach is to use the create() method. I would use the AQC design pattern for clean code.
<?php
namespace AppAQCOrder;
use AppModelsOrder;
class CreateOrder
{
public static function handle($params)
{
Order::create($params);
}
}
This is the data we want to save as is. No change in user-provided data. Simple, right?
2. Do Calculations
Let’s say we want to do some calculations for some columns based on the data posted by the user when saving. Where would you write the logic? Before the create() method?
<?php
namespace AppAQCOrder;
use AppModelsOrder;
use AppConstantsOrderStatus;
class CreateOrder
{
public static function handle($params)
{
$data = $params;
if ($data["discount_percentage"]) {
$data["discount_price"] = $data["price"] * (1 - $data["discount_percentage"] / 100);
}
$data['status'] = OrderStatus::Pending;
Order::create($data);
}
}
But I would rather use Observer here. If you don’t know what Observers are, you can read here.
https://laravel.com/docs/12.x/eloquent#observers
<?php
namespace AppObservers;
use AppConstantsOrderStatus;
class OrderObserver
{
public function creating(Order $order)
{
if ($order->discount_percentage) {
$order->discount_price = $order->price * (1 - $order->discount_percentage / 100);
}
$order->status = OrderStatus::Pending;
}
}
This method will be defined in the OrderObserver class, and the observer class will be observing the Order model.
<?php
namespace AppModels;
use AppObserversOrderObserver;
use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentAttributesObservedBy;
#[ObservedBy([OrderObserver::class])]
class Order extends Model
{
// other code
}
If you want to avoid triggering of Observable events in any case, you can use quite methods like saveQuietly().
3. Generate data from Internals
Let’s say the order needs an incremental order number, so we need a function that should generate the order number by querying how many orders have already been saved. Get the latest count and generate a new order number. What would you do?
<?php
namespace AppAQCOrder;
use AppModelsOrder;
class CreateOrder
{
public static function handle($params)
{
$data = $params;
if ($data["discount_percentage"]) {
$data["discount_price"] = $data["price"] * (1 - $data["discount_percentage"] / 100);
}
$data['order_number'] = self::generateOrderNumber();
Order::create($data);
}
private static function generateOrderNumber()
{
$count = Order::count();
return 'ORDER-' . str_pad($count + 1, 5, '0', STR_PAD_LEFT);
}
}
Again, I would move this logic of internal query call to Observer.
<?php
namespace AppObservers;
use AppModelsOrder;
use AppConstantsOrderStatus;
class OrderObserver
{
public function creating(Order $order)
{
if ($order->discount_percentage) {
$order->discount_price = $order->price * (1 - $order->discount_percentage / 100);
}
$order->status = OrderStatus::Pending;
$count = Order::count();
$order->order_number = 'ORDER-' . str_pad($count + 1, 5, '0', STR_PAD_LEFT);
}
}
This seems the correct location for this kind of work. We have moved our logic into the creating() method, telling Laravel to do this additional work before saving data to the database.
4. Trigger Events
Let’s say we want to notify the user that his order has been placed via email. Where would you write the call to trigger event? In controller?
<?php
namespace AppAQCOrder;
use AppModelsOrder;
use AppEventsNotifyOrderCreation;
class CreateOrder
{
public static function handle($params)
{
$data = $params;
if ($data["discount_percentage"]) {
$data["discount_price"] = $data["price"] * (1 - $data["discount_percentage"] / 100);
}
$data['order_number'] = self::generateOrderNumber();
$order = Order::create($data);
event(new NotifyOrderCreation($order));
}
private static function generateOrderNumber()
{
$count = Order::count();
return 'SKU' . str_pad($count + 1, 5, '0', STR_PAD_LEFT);
}
}
Again, I will write this event-triggering logic to Observer but in the created() method this time because at this point Order has been saved.
<?php
namespace AppObservers;
use AppModelsOrder;
use AppEventsNotifyOrderCreation;
use AppConstantsOrderStatus;
class OrderObserver
{
public function creating(Order $order)
{
if ($order->discount_percentage) {
$order->discount_price = $order->price * (1 - $order->discount_percentage / 100);
}
$order->status = OrderStatus::Pending;
$count = Order::count();
$order->order_number = 'ORDER-' . str_pad($count + 1, 5, '0', STR_PAD_LEFT);
}
public function created(Order $order)
{
event(new NotifyOrderCreation($order));
}
}
Summary
Instead of writing everything into a single class, we have now distributed the code chunks to different parts of observers, which seems the right place. Once we are done, we don’t have to worry about how we complete these steps if we place orders from different APIs.
Business logic doesn’t belong in controllers and services, and models shouldn’t be dumb shells waiting for orders.
The Laravel Observers feature gives us the feasibility to perform actions before and after performing any action on the database. It works similarly to events in a database but gives more control.
By using Observers, you let your models take responsibility for their own behavior. Calculations, data generation, conditional logic, and event firing — these are all things the model should handle itself, not some controller or service.
This separation keeps your architecture lean and expressive. AQCs handle intent. Observers handle behavior. Controllers stay clean.
Let the model own its business. That’s how you write sustainable Laravel.
If you found this post helpful, consider supporting my work — it means a lot.