Queopius

Eloquent models too large

The problem is not Eloquent. The problem appears when the model becomes the place where everything ends: queries, business rules, transformations, product decisions, permissions and presentation logic.

Laravel Health Check5 min
Featured image from the article Eloquent Models Too Large published in LinkedIn by Queopius
Laravel Health CheckEloquentTechnical debtArchitecture

The problem is not Eloquent, it is the accumulation of responsibilities

Eloquent is one of the most productive pieces of Laravel. It allows you to model relationships, query data and express part of the domain's behavior in a very comfortable way. That comfort is precisely what makes many projects begin to leave each new rule within the model.

At first it seems like a good decision: the code is close to the data and everything moves quickly. The problem arises when the model stops representing an entity and begins to coordinate complete processes. At that point, changing a business rule, a API response, or a critical query may involve touching a class that already concentrates too many reasons to change.

A large model is not bad just because it has many lines. It is worrying when you mix persistence, business, presentation, context HTTP and coordination of side effects. This mixture blurs the boundary between “what this entity is” and “what the system does when something happens with this entity.”

Signs that a model is growing too much

A first sign is to find long methods that do not describe natural behavior of the model. For example, methods that prepare a complete response for an endpoint, calculate business decisions with many conditions, or coordinate multiple external models. The name may sound innocent, but the content no longer belongs to the entity.

It is also worth looking at the scopes. A small, expressive scope can be useful. A set of scopes that accumulates conditional filters, user dependencies, visibility rules, product ordering and permissions logic begins to behave like a Query Object hidden within the model.

Other common signs are accessors and mutators that resolve product decisions, indirect dependency on Request, Auth, session or configuration, API responses prepared within the model, business rules mixed with persistence and tests that can only be covered by creating too much context around them.

Why does so much happen in Laravel

Laravel allows you to move forward quickly and that is a real advantage. In a first version, putting logic close to Eloquent can reduce friction and help validate product. The problem is not in starting simple, but in not reviewing the limits when the system changes scale.

Many projects go from simple CRUD to production product without a clear application layer. When a new rule appears, the model seems like the natural place to drop it because it already has relationships, attributes, and database access. If that decision is repeated for months, the model ends up acting as a service, query layer, transformer and policy at the same time.

It is not a bug of Laravel. It is a normal consequence of growing with delivery pressure and without a periodic technical review. Precisely for this reason it is important to detect the pattern before the model becomes a piece that no one wants to touch.

What risks does it generate?

The most visible risk is slowness. Each change requires understanding more context than necessary because the class no longer has a clear responsibility. An adjustment to a business rule can affect a query, a API transformation, or an asynchronous operation that lives in the same file.

Fear of touching code also appears. When a model concentrates too much logic, the team stops relying on small changes. Tests are more difficult to write because each method carries persistence, relationships, factories and secondary states. Even optimizing queries becomes more complex because the query is mixed with business decisions.

In the medium term, this pattern generates coupling between the model, API and product rules. Reusing logic in a new command, job, or endpoint forces you to load more dependencies than necessary. Technical debt is silent because the application can continue to work, but each iteration costs more.

How to start dividing responsibilities

The first criterion is to preserve in the model the essential behavior of the entity: relationships, casts, relevant attributes, small rules specific to the state and methods that really describe something that the model “is” or “knows”. If the method coordinates a process, it probably belongs to another layer.

Use cases can be extracted to Services or Actions. Complex queries can live in Query Objects, smaller scopes, or specific classes. The validation HTTP should be in Form Requests. The output transformation best fits API Resources. Authorization should be delegated to Policies or Gates. Heavy or deferred processes usually belong to Jobs.

There is no need to create a huge architecture all at once. It makes more sense to start with the areas with the most change, the most bugs or the most risk. A well-chosen extraction reduces friction immediately; an extraction for aesthetics can add complexity without improving the capacity for evolution.

Conceptual example

Imagine an Order model that calculates trading status, prepares the API payload, applies advanced filters, checks permissions, and triggers notifications. None of that necessarily breaks production, but it turns Order into a piece with too many responsibilities.

A clearer separation could leave Order with essential relationships, attributes, and behavior. CreateOrderAction or OrderService would handle the use case. OrderQuery would solve complex filters. OrderResource would transform the output to API. OrderPolicy would decide permissions. A Job would manage notifications or heavy processes.

Improvement is not “having more classes.” The improvement is that each future change has a more obvious place. If you change the answer from API, you don't touch the model. If the authorization changes, you do not touch the query. If the creation process changes, you don't mix that decision with presentation.

Quick checklist

Before accepting another method within the model, it is worth asking: does it represent the entity's own behavior or does it coordinate a use case? Does it depend on HTTP, Request, Auth, session or external context? Are you preparing a response for frontend or API? Does it contain a complex query that is difficult to reuse?

It also helps to check if you mix business rules with persistence, if it would be clearer as Action, Service, Query Object, Resource or Policy, and if it can be tested without building too much state around it. If the answer to several questions is negative, the model is probably absorbing responsibilities that should be separated.

Closing

A large model does not always break the application, but it usually indicates that the architecture is losing its limits. The important signal is not the exact number of lines, but the number of different decisions that coexist in the same class.

If your Laravel works, but each change starts to touch too many pieces, a Backend Audit Laravel can help you detect which responsibilities should be separated first and which quick wins have the most real impact.

Does your Laravel show similar signals?

If your application works, but each change begins to be slower, more fragile or more difficult to explain, a Backend Audit Laravel can help you sort out risks, quick wins and technical priorities.