Introducing The Atomic Query Construction (AQC) Design Pattern

Introducing The Atomic Query Construction (AQC) Design Pattern

posted Originally published at raheelshan.com 5 min read

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

“While I’ll use Laravel in the code examples, this pattern is framework-agnostic and can be adapted to any OOP-based backend environment — Node.js, PHP, Python, etc.”

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.

“With AQC, every query has a home. No more hunting across layers of your app to find and fix logic. Write it once. Use it everywhere. Welcome to clean, atomic thinking.”

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’d like personal mentorship or 1-on-1 help with AQC Design Pattern, I’m available on Topmate.io here.

Schedule a booking


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

Support My Work

More Posts

Don’t Pass Array or Variables to Laravel Blade Views Instead Do This

Raheel - Apr 8

Local-First: The Browser as the Vault

Pocket Portfolioverified - Apr 20

Sanitization by Construction: The "Edge Compiler"

Pocket Portfolioverified - Apr 13

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelskiverified - Apr 23

Sovereign Intelligence: The Complete 25,000 Word Blueprint (Download)

Pocket Portfolioverified - Apr 1
chevron_left

Related Jobs

Commenters (This Week)

2 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!