DEV Community

Cover image for Stop Writing Queries Everywhere: The Atomic Habit Your Laravel Codebase Needs
Raheel Shan
Raheel Shan

Posted on

Stop Writing Queries Everywhere: The Atomic Habit Your Laravel Codebase Needs

  • Atomic Query Construction (AQC)
  • Backend Development
  • Software Architecture
  • Clean Code

You Have a Query Problem. You Just Haven’t Admitted It Yet.

Let’s describe a normal Laravel codebase.

  • A controller fetches active users
  • A job fetches almost the same users, with one extra condition
  • A service fetches them again, missing a condition
  • A test rebuilds the query from scratch

Same intent. Different queries. Different results.

Nobody notices until something breaks. Then everyone starts guessing which version is “correct”.

That’s the problem.

Not complexity. Not scale.

Inconsistency.


The Problem Isn’t Duplication. It’s Drift.

People love saying “don’t repeat yourself”.

They still write this everywhere:

User::where('active', true)->get();
Enter fullscreen mode Exit fullscreen mode

Then somewhere else:

User::where('active', true)
    ->whereNotNull('email_verified_at')
    ->get();
Enter fullscreen mode Exit fullscreen mode

Then somewhere else:

User::where('active', true)
    ->where('role', 'admin')
    ->get();
Enter fullscreen mode Exit fullscreen mode

Now you don’t have duplication.

You have different definitions of the same thing.

That’s worse.

Because now your system behaves differently depending on which file you opened.

The Rule: One Layer Owns All Queries

There is only one way to stop this:

Every database query must live in one layer. That layer is AQC.

And once it exists:

  • Controllers do not write queries
  • Services do not write queries
  • Jobs do not write queries
  • Tests do not write queries

If a query exists outside AQC, it’s wrong. No debate. Its violation of AQC.

Instead of this in a controller:

public function index(Request $request)
{
    $users = User::where('active', true)
        ->whereNotNull('email_verified_at')
        ->get();
    return view('users.index',compact('users'));
}
Enter fullscreen mode Exit fullscreen mode

You do this:

public function index(Request $request)
{
    $users = (new GetUsers())->handle(['active' => true]);
    return view('users.index',compact('users'));
}
Enter fullscreen mode Exit fullscreen mode

Now the query has one home.

Same Rule Everywhere

Controller

$users = (new GetUsers())->handle([
    'role' => $request->role,
]);
Enter fullscreen mode Exit fullscreen mode

Service

$users = (new GetUsers())->handle([
    'digest_enabled' => true,
]);
Enter fullscreen mode Exit fullscreen mode

Job

$users = (new GetUsers())->handle([
    'active' => false,
]);
Enter fullscreen mode Exit fullscreen mode

Test

$users = (new GetUsers())->handle([
    'active' => true,
]);
Enter fullscreen mode Exit fullscreen mode

Nobody writes queries anymore.

They just use them.

And guess what? You have composition.

With AQC, you can make a single query class handle multiple contexts, without repeating yourself or risking divergence.

Look at this example:

<?php

namespace App\AQC\User;

use App\Models\User;

class GetUsers
{
    public function handle(array $params = [])
    {
        $query = User::query();

        // apply filters conditionally
        if (!empty($params['active'])) {
            $query->where('active', $params['active']);
        }

        if (!empty($params['digest_enabled'])) {
            $query->where('digest_enabled', $params['digest_enabled']);
        }

        if (!empty($params['role'])) {
            $query->where('role', $params['role']);
        }

        // Apply sorting when requested otherwise do it on id by default
        if(isset($params['sortBy']) && isset($params['type'])){
            $sortBy = $params['sortBy'];
            $type = $params['type'];    
            $query->orderBy($sortBy, $type);
        }

        return isset($params['paginate'])
            ? $query->paginate(User::PAGINATE)
            : $query->get();   
    }
}
Enter fullscreen mode Exit fullscreen mode

Here’s what’s happening:

  1. Conditional filters – Active users, digest-enabled users, role-based filtering – all controlled by the caller. Nothing is hard-coded in multiple places.
  2. Flexible sorting – The query supports dynamic ordering, but defaults to a predictable behavior if nothing is specified.
  3. Pagination or full collection – One class handles both paginated lists for the UI and full collections for jobs or exports.

This is composition in action. One query class serves multiple consumers, each passing their parameters. The base conditions and logic live in one place. No controller, service, or job reimplements a filter or accidentally forgets a condition.

The beauty? Every consumer gets exactly what it needs without duplicating or diverging query logic. You maintain a single source of truth for fetching users.

What You Gain (And What You Stop Losing)

When queries live in one place:

  • You stop redefining business rules
  • You stop forgetting conditions
  • You stop chasing bugs across files
  • You stop guessing which query is correct

Change happens once. Behavior updates everywhere.

That’s it.

What Counts as a Violation

Let’s make this uncomfortable:

If you write this anywhere outside AQC:

User::where(...)
Enter fullscreen mode Exit fullscreen mode

You just broke the architecture. You just violated the AQC Design pattern rule.

Doesn’t matter if it’s “just one condition” Doesn’t matter if it’s “temporary” Doesn’t matter if it’s “faster this way”

It’s wrong.

AQC only works if it’s enforced. Not suggested. But enforced.

The Habit

Before writing any query, there are only two options:

  • It already exists → use it
  • It doesn’t exist → create it in AQC

There is no third option.

Final Thoughts

Atomic Query Construction removes the repeatitive queries by giving every query a single home. One layer. One class per operation. One public method. One parameter signature. Every consumer uses the same query, every time.

Controllers call AQC classes. Services call AQC classes. Jobs call AQC classes. Tests call AQC classes. The database answers to one layer — and one layer only.

Stop guessing. Stop duplicating. Stop chasing subtle divergences across files. Build the layer, enforce the rules, and make query discipline a habit.

This is not optional. This is the foundation of a maintainable, predictable Laravel codebase.

Top comments (0)