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
One class = one intention. Parameters define the variations.
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.
Beware: Passing 2 parameters togather can result in not found.
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);
}
}
The parameters are the source of truth. No mapping or rigid arguments required. Create is straightforward.
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']);
}
}
One class handles updating multiple fields, conditionally, depending on what parameters exist. No need for separate methods like
updatePrice()orupdateStock().
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();
}
}
Conditional deletion using the same class for multiple delete intentions. Flexible, atomic, and predictable.
Why This Works
- Parameter-driven: No rigid methods. Flexible to multiple scenarios.
- Single class = single intention: Each class is atomic.
- Controllers stay thin: All query logic is inside AQC.
- No dependency injection required: Eloquent is enough, because these queries have fixed intentions.
- 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.

Top comments (0)