When I first introduced the Atomic Query Construction (AQC) design pattern, the focus was on breaking down monolithic repositories into small, single-purpose query classes. These classes are driven entirely by parameters, not hard-coded logic. That philosophy allows a single class to handle multiple query intentions without method explosion, while keeping code readable and maintainable.

In this article, I want to show you how to implement AQC for full CRUD operations, following the same parameter-first approach. This is the practical way to apply the pattern in a Laravel application but you are free to adopt this pattern in any language of your preference.

The Philosophy Recap

AQC is based on one principle

Folder Structure

A simple organization for CRUD operations:

                                app/
 └─ AQC/
     ├─ Product/
     │   ├─ GetProducts.php
     │   ├─ GetProduct.php
     │   ├─ CreateProduct.php
     │   ├─ UpdateProduct.php
     │   └─ DeleteProduct.php
     │
     └─ User/
         ├─ GetUsers.php
         ├─ GetUser.php
         ├─ CreateUser.php
         ├─ UpdateUser.php
         └─ DeleteUser.php
                            

1. Fetch Multiple Records (GetProducts)  

                                <?php

namespace App\AQC\Product;

use App\Models\Product;

class GetProducts
{
    public function handle($params = [])
    {
        $productObj = Product::latest('id');
		
		// apply filters
        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 filters according to your requirements

		// select columns
		if(isset($params['columns']) && ($params['columns']) > 0){
			$productObj->select($params['columns']);
		}else{
			$productObj->select('*');
		}        

        // 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'])
            ? $productObj->paginate(Product::PAGINATE)
            : $productObj->get();    
    }
}
                            

if handle method grows create internal helper methods.

                                <?php

namespace App\AQC\Product;

use App\Models\Product;

class GetProducts
{
    public function handle($params = [])
    {
        $query = Product::latest('id');
        
        $this->applyFilters($query, $params);
        $this->selectColumns($query, $params);
        $this->applySorting($query, $params);

        return isset($params['paginate'])
            ? $this->handlePagination($query, $params)
            : $query->get();    
    }
  
    private function applyFilters($query, $params) { /* ... */ }
    private function selectColumns($query, $params) { /* ... */ }
    private function applySorting($query, $params) { /* ... */ }
    private function handlePagination($query, $params) { /* ... */ }
}
                            

2. Fetch a Single Record (GetProduct)  

                                <?php

namespace App\AQC\Product;

use App\Models\Product;

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

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

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

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

        return $query->firstOrFail();
    }
}
                            

Product can be fetched based on id, slug or sku.

3. Create a Record (CreateProduct)  

                                <?php

namespace App\AQC\Product;

use App\Models\Product;

class CreateProduct
{
    public function handle(array $params)
    {
        return Product::create($params);
    }
}
                            

4. Update a Record Conditionally (UpdateProduct)  

                                <?php

namespace App\AQC\Product;

use App\Models\Product;

class UpdateProduct
{
    public function handle($params = [])
    {
        $productObj = Product::query();

        // Conditional WHERE clauses
        if (!empty($params['id'])) {
            $productObj->where('id', $params['id']);
        }

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

        // update provided columns only
        return $productObj->update($params['columns']);
    }
}
                            

5. Delete Records Conditionally (DeleteProduct)  

                                <?php

namespace App\AQC\Product;

use App\Models\Product;

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

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

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

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

        return $query->delete();
    }
}
                            

Why This Works

  1. Parameter-driven: No rigid methods. Flexible to multiple scenarios.
  2. Single class = single intention: Each class is atomic.
  3. Controllers stay thin: All query logic is inside AQC.
  4. No dependency injection required: Eloquent is enough, because these queries have fixed intentions.
  5. Reusable atomic filters: Common filters (e.g., active, status, role) can be moved into static helpers to be applied across multiple query classes.

Final Thoughts

AQC isn’t about complexity. It’s about clarity, atomicity, and flexibility.

  • Fetch multiple records? Use GetProducts with parameters.
  • Fetch a single record? Use GetProduct.
  • Create, update, delete? Each has its own atomic class with conditional behavior.

When applied correctly, AQC keeps controllers thin, and queries flexible. It scales with your application without creating method explosion or unnecessary abstraction layers.

Comments


Comment created and will be displayed once approved.