When I started my career as a developer, one of my senior team members used to construct queries in core PHP using concatenation based on different parameters. Once the construction was complete, he would echo it for different use cases and run it in PHPMyAdmin or SQLyog. Once satisfied, he’d move on. I always liked the way he built those queries.

Later, when I moved on to CodeIgniter and eventually Laravel, I noticed something: Developers often write SQL or ORM-based queries buried deep inside controllers, services, views, event listeners — even helpers. This scattered approach leads to redundancy and painful maintenance. We’ve all seen this.

This always bugged me. Why repeat the same logic scattered across the application when it can be centralized? 

Being an OOP geek, I finally settled on a pattern that works cleanly and elegantly. To counter this problem, I am introducing a new design principle: Atomic Query Construction — or AQC. It’s a design approach where each query lives in its own class. You write it once and use it everywhere. It’s structured, predictable, and aligned with better software design principles.


What is AQC?

AQC stands for Atomic Query Construction. At its core, it’s a principle of isolating each query into its own class. Each of these classes:

  • Accepts Parameters
  • Constructs Query based on those parameters for different use cases
  • Returns Results

You only call this class whenever that specific query is needed — nothing else. It becomes the single source of truth for that operation.


Rules of the Pattern

1. One Class, One Responsibility

Every class should focus on a single, well-defined query. Stick to the Single Responsibility Principle (SRP).

2. Accept Parameters

Each class should accept parameters — filters, identifiers, flags — whatever’s needed to shape the query dynamically.

3. Construct the Query Internally

Based on the parameters, the class should build and return a query.

4. Single Source of Truth

Anywhere in the app where you need this data — whether in a controller, event, or service — call this class. Do NOT rewrite the query logic elsewhere.

5. Handle Method

Each class should expose a single public method, handle(). It can have private helpers internally, but the interface stays clean.

6. Consistent Naming

Class names must be clear and consistent. For a resource like Product, your AQC classes might be:

  • GetAllProducts
  • GetProduct
  • StoreProduct
  • UpdateProduct
  • DeleteProduct

Benefits

  • Modular: Every query is isolated and self-contained. You know exactly where to look when changes are needed.
  • Reusable: Once written, the query class can be reused across the app with different params or use cases.
  • Flexible: It supports multiple scenarios (admin view, frontend listing, etc.) using dynamic input.
  • Clean Separation: Keeps your controllers, services, and views clean. No query logic there — just calls to your AQC classes.
  • Testable: Because logic is in isolated classes, it’s easy to write unit tests without involving controllers or database state.

Implementation Example

Let’s say we’re working with a Product model in a typical eCommerce app. First, create a folder:  

                                app/AQC/Product/
                            
  • GetAllProducts.php
  • GetProduct.php
  • StoreProduct.php
  • UpdateProduct.php
  • DeleteProduct.php

GetAllProducts.php

                                <?php

namespace App\AQC\Product;

use App\Models\Product;

class GetAllProducts
{
    public static function handle($params = [], $paginate = true, $scenario = 'default')
    {
        $productObj = Product::latest('id');

        if (isset($params['category_id']) && $params['category_id'] > 0) {
            $productObj->where('category_id', $params['category_id']);
        }

        if (isset($params['brand_id']) && $params['brand_id'] > 0) {
            $productObj->where('brand_id', $params['brand_id']);
        }

        // add more conditions for different use cases

        switch ($scenario) {
            case 'minimal':
                $productObj->select(['id', 'name']);
                break;
            case 'compact':
                $productObj->select(['id', 'name', 'price', 'image']);
                break;
            case 'admin':
                $productObj->select(['id', 'name', 'price', 'sku', 'image', 'stock', 'cost']);
                break;
            default:
                $productObj->select('*');
        }

        return $paginate
            ? $productObj->paginate(Product::PAGINATE)
            : $productObj->get();
    }
}
                            

Usage:

                                // Admin listing
$products = GetAllProducts::handle(); 
// return paginated product order id latest 

// Frontend product listing
$products = GetAllProducts::handle($request->all(), true, 'compact');
                            

GetProduct.php

                                <?php

namespace App\AQC\Product;
use App\Models\Product;

class GetProduct
{
    public static function handle($id, $params = [], $scenario = 'default')
    {
        $productObj = Product::where('id', $id);

        if (isset($params['active']) && $params['active']) {
            $productObj->where('active', $params['active']);
        }

        // add more conditions for different use cases

        switch ($scenario) {
            case 'minimal':
                $productObj->select(['id', 'name']);
                break;
            case 'compact':
                $productObj->select(['id', 'name', 'price', 'image']);
                break;
            case 'admin':
                $productObj->select(['id', 'name', 'price', 'sku', 'image', 'stock', 'cost']);
                break;
            default:
                $productObj->select('*');
        }

        return $productObj->first();
    }
}
                            

Usage:

                                // Admin edit screen
$product = GetProduct::handle($productID, [], 'admin');

// Frontend product detail
$product = GetProduct::handle($productID, [], 'compact');
                            

StoreProduct.php

                                <?php

namespace App\AQC\Product;
use App\Models\Product;

class StoreProduct
{
    public static function handle($params)
    {
        $product = new Product();
        $product->fill($params);

        if (isset($params['type']) && $params['type'] === 'combo') {
            $product->is_combo = true;
        }

        if (!isset($params['sku'])) {
            $product->sku = self::generateSku($params);
        }

        $product->save();
        return $product;
    }

    private static function generateSku($params)
    {
        // SKU generation logic
        return 'SKU-' . rand(1000, 9999);
    }
}
                            

UpdateProduct.php

                                <?php

namespace App\AQC\Product;
use App\Models\Product;

class UpdateProduct
{
    public static function handle($id, $params)
    {
        $product = Product::find($id);

        if (!$product) return null;

        if (isset($params['cost'])) $product->cost = $params['cost'];
        if (isset($params['price'])) $product->price = $params['price'];
        if (isset($params['stock'])) $product->stock = $params['stock'];

        // add more fields as needed

        $product->save();
        return $product;
    }
}
                            

DeleteProduct.php

                                <?php

namespace App\AQC\Product;
use App\Models\Product;

class DeleteProduct
{
    public static function handle($params = [])
    {
        $productsObj = Product::query();

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

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

        if (!empty($params['product_id'])) {
            $productsObj->where('id', $params['product_id']);
        }

        if (!$product) return false;

        $productObj->delete();

        return true;
    }
}
                            

Final Thoughts

Each class builds the query, covers various scenarios, and cleanly separates logic. You can now reuse queries across your entire app — controllers, API endpoints, and admin panels — without rewriting anything.

When a new requirement comes in, you know exactly where to go. No more chasing query logic across 20 files.

If you try AQC in your projects, I’d love to hear how it works for you — or what you’d improve.

I’ll be sharing more Laravel architecture patterns soon. Follow along if you’re into clean, scalable code.


If you found this post helpful, consider supporting my work — it means a lot.

 Raheel Shan | Support my work
Raheel Shan | Support my work
Support my work
raheelshan.com
Comments


Comment created and will be displayed once approved.