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

Published: (March 23, 2026 at 03:19 AM EDT)
4 min read
Source: Dev.to

Source: Dev.to

Topics

  • 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,” yet they still write this everywhere:

User::where('active', true)->get();

Then somewhere else:

User::where('active', true)
    ->whereNotNull('email_verified_at')
    ->get();

Then somewhere else:

User::where('active', true)
    ->where('role', 'admin')
    ->get();

Now you don’t have duplication.
You have different definitions of the same thing.
That’s worse, because 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.

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. It violates AQC.

Before (controller)

public function index(Request $request)
{
    $users = User::where('active', true)
        ->whereNotNull('email_verified_at')
        ->get();

    return view('users.index', compact('users'));
}

After (controller)

public function index(Request $request)
{
    $users = (new GetUsers())->handle(['active' => true]);

    return view('users.index', compact('users'));
}

Now the query has one home.

Same Rule Everywhere

Controller

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

Service

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

Job

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

Test

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

Nobody writes queries anymore. They just use them. And guess what? You have composition.

AQC in Action

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 sort by id
if (isset($params['sortBy']) && isset($params['type'])) {
    $query->orderBy($params['sortBy'], $params['type']);
}

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

What’s happening?

  • Conditional filters – Active users, digest‑enabled users, role‑based filtering – are all controlled by the caller. Nothing is hard‑coded in multiple places.
  • Flexible sorting – The query supports dynamic ordering, but defaults to a predictable behavior if nothing is specified.
  • 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 its own parameters. The base conditions and logic live in one place. No controller, service, or job re‑implements 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

If you write this anywhere outside AQC:

User::where(...);

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 merely suggested.

The Habit

Before writing any query, there are only two options:

  1. It already exists → use it
  2. It doesn’t exist → create it in AQC

ist → create it in AQC

There is no third option.

Final Thoughts

Atomic Query Construction removes the repetitive 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.

0 views
Back to Blog

Related posts

Read more »

⛑️ Kotlin Support in VSCode

Introduction My current job uses 🧑‍💻️ Kotlin as the main backend programming language, and I’m a huge fan of VS Code. Naturally, I wanted to set up Kotlin de...