Laravel ERP Architecture: Structure That Lasts
How to structure a large Laravel ERP application: modules by business domain, thin controllers, action classes, events and clear module boundaries.
Laravel's default structure is ideal for a small application. An ERP is not small. It has inventory, sales, purchasing, production and accounting, each with dozens of screens, and it will be maintained for ten years by people who have not joined yet. This is the structure I use to keep a system like that understandable.
Organize by business module, not by file type
With hundreds of models, a single app/Models folder tells you nothing. Group code by the part of the business it serves, so everything about inventory lives together.
app/Modules/
Inventory/
Models/ Actions/ Http/ Events/ Policies/ Reports/
Sales/
Models/ Actions/ Http/ Events/ Policies/ Reports/
Purchasing/
Accounting/
app/Shared/ (users, approvals, numbering, audit)A new developer asked to fix a sales bug then has one folder to learn, not the whole codebase.
Keep controllers thin
A controller should validate input, call one piece of business logic and return a response. Validation goes in Form Requests and authorization in Policies. If a controller method is longer than a screen, business rules have leaked into it.
One action class per business operation
Put each operation, such as confirming a sales order, in its own class with a single public method. The same action can then be called from a web controller, an API controller, a queued job or an import, and the rule exists in exactly one place.
class ConfirmSalesOrder
{
public function __construct(private ReserveStock $reserveStock) {}
public function execute(SalesOrder $order, User $by): SalesOrder
{
return DB::transaction(function () use ($order, $by) {
$order->ensureCanBeConfirmed();
$this->reserveStock->execute($order);
$order->markConfirmed($by);
SalesOrderConfirmed::dispatch($order);
return $order;
});
}
}Transactions wrap the whole operation
Confirming an order changes the order, the stock and possibly the customer's balance. Either all of it happens or none of it does. The transaction belongs in the action, around the complete business step, not inside individual model methods.
Let modules talk through events and contracts
Sales should not reach into inventory's tables. It should call a public action that inventory offers, or dispatch an event such as SalesOrderConfirmed that accounting listens to. This keeps modules replaceable and makes side effects, such as notifications, easy to move to queues.
Shared services every ERP needs
- Document numbering that is gap-free and safe under concurrent use.
- Role and permission management, checked through Policies.
- An audit log of who changed what.
- A generic approval engine. See designing an approval workflow engine in Laravel.
- Fiscal periods and period locking, so closed months cannot be changed.
Keep reports separate
Reports read across many tables and have different performance needs from transactions. Give them their own query classes, written with the query builder or SQL rather than chains of Eloquent relationships. They are then easy to optimize or move to a reporting database later.
Test the rules that cost money
You will not test everything. Prioritize feature tests for stock movements, pricing, tax and posting logic, where a silent error becomes a financial error. Action classes make this simple, because each rule can be tested without going through HTTP.
Structure only helps if the data underneath is sound. The companion to this post is ERP database design mistakes. If you are still deciding whether to build at all, start with custom ERP vs SAP.
Common questions
Can Laravel be used to build an ERP system?
Yes. Laravel is well suited to custom ERP modules when the application is organized into business modules with clear boundaries, transactional business logic and a carefully designed database.
How should a large Laravel application be structured?
Group code by business domain rather than file type, keep controllers thin, place each business operation in its own action or service class, and let modules communicate through events and defined interfaces.
Where should business logic go in Laravel?
In dedicated action or service classes, wrapped in database transactions. Controllers handle HTTP concerns and models handle persistence and simple state rules.