In the last two posts, I introduced the Atomic Query Construction (AQC) design pattern and showed how it transformed my Laravel architecture. If you haven’t read those yet, I recommend checking them out first:
https://raheelshan.com/posts/introducing-the-atomic-query-construction-aqc-design-pattern
https://raheelshan.com/posts/laravel-architecture-evolved-aqc-took-over-everything
In this post, I’ll finally break down AQC in action — with detailed use cases and examples.
A Real-World Scenario
Let’s walk through a typical eCommerce system. Imagine it’s made of the following components:
- Admin Control Panel (ACP) internal back-office operations
- E-commerce Website (EW) public-facing storefront
- Mobile Application (MA) companion app for mobile users
We’ll focus on the product resource and see how often it gets queried—under different use cases, from different sources. This will demonstrate why you need a consistent, centralized, and context-aware query mechanism like AQC.
Use Cases: Admin Control Panel (ACP)
In the ACP, the admin might fetch a list of products when:
- Viewing all products with pagination
- Applying filters (by category, brand, status, etc.)
- Exporting products to an Excel sheet (to restock later)
- Selecting products in a dropdown, e.g., while creating a purchase order or a GRN
- Generating reports like
— Product Inventory
— Low Stock
— Out of Stock
— Expired Products
Each use case may need different columns, filters, sorting rules, or joins.
Use Cases: E-commerce Website (EW)
The same Productresource is used in the storefront. Users might see it.
- On the homepage, under sections like Trending, New Arrivals, or Featured
- In search results (keyword + paginated)
- In category listings (paginated)
- In filtered views (e.g., sort by price, filter by brand)
- On the product detail page, where related products, accessories, or upsells are shown
All these views need variations of the same query, tailored for speed, UX, and front-end design.
Use Cases: Mobile Application (MA)
The mobile app mimics the frontend behavior;
Again, same resource, different context, different needs.
Why This Matters
Without a pattern like AQC, you’d be duplicating query logic across
- Controllers
- Services
- Resources
- Custom exports
- Report generators
And every time a column or filter changes, you’d need to find the exact place and fix it. That’s tech debt on steroids.
With AQC, your query lives in a single-responsibility class under a consistent namespace like
app/AQC/Product/GetAllProducts.php
Now lets write the class.
<?php
namespace App\AQC\Product\GetAllProducts;
use App\Models\Product;
class GetAllProducts
{
public static function handle($params = [])
{
$products = [];
$query = Product::latest('id');
self::applyFilters($query , $params);
self::selectColumns($query , $params);
self::applySorting($query , $params);
if(isset($params['paginate'])){
$products = self::handlePagination($query , $params);
} else {
$products = $query->get();
}
// transform the results if required
return $products;
}
private static function applyFilters($query , $params)
{
// apply requested filters
if (isset($params['category_id']) && $params['category_id'] > 0) {
$query->where('category_id', $params['category_id']);
}
if (isset($params['brand_id']) && $params['brand_id'] > 0) {
$query->where('brand_id', $params['brand_id']);
}
if (isset($params['keyword'])) {
$query->where('name', 'like', '%' . $params['keyword'] . '%')
->orWhere('sku', 'like', '%' . $params['keyword'] . '%')
->orWhere('description', 'like', '%' . $params['keyword'] . '%');
}
if (isset($params['low_stock'])) {
$query->where('stock', '<=' , 'low_stock_point');
}
if (isset($params['out_of_stock'])) {
$query->where('stock', 0);
}
if (isset($params['expired'])) {
$query->whereDate('expired_date', '<', today())->get();
}
if (isset($params['featured'])) {
$query->where('is_featured', true);
}
if (isset($params['trending'])) {
$query->where('is_trending', true);
}
if (isset($params['product_id'])) {
$query->where('related_product_id', $params['product_id']);
}
return $query;
}
private static function selectColumns($query , $params)
{
$scenario = isset($params['scenario']) ? $params['scenario'] : 'default';
// select columns based on requested scenario
switch ($scenario) {
case 'minimal':
$query->select(['id', 'name']);
break;
case 'compact':
$query->select(['id', 'name', 'price', 'image']);
break;
case 'export':
$query->select(['id', 'name', 'price', 'sku', 'cost']);
break;
case 'admin':
$query->select(['id', 'name', 'price', 'sku', 'image', 'stock', 'cost']);
break;
default:
$query->select('*');
}
return $query;
}
private static function applySorting($query , $params)
{
// Apply sorting when requested otherwise do it on it by default
if(isset($params['sortBy']) && isset($params['type'])){
$sortBy = $params['sortBy'];
$type = $params['type'];
$query->orderBy($sortBy, $type);
}
return $query;
}
private static function handlePagination($query , $params)
{
// if paginaton is requested by api check per_page and page otherwise do the default pagination
if(isset($params['per_page']) && isset($params['page'])){
return $query->paginate($params['per_page'], ['*'], 'page', $params['page']);
}else{
return $query->paginate(Product::PAGINATE);
}
}
}
OK. This is our AQC class, neat and clean. And finally, I need to call this class on all the above scenarios so that it can construct a query and generate the results based on parameters. To accomplish all, let's add missing pieces.
Now we need a simple controller method for this:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\AQC\Product\GetAllProducts;
use App\Core\ResponseHelper;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function index(Request $request)
{
$params = $request->all();
$products = GetAllProducts::handle($params);
return ResponseHelper::handle('index', [ 'products' => $products ]);
}
}
Our ResponseHelper class:
<?php
namespace App\Core\ResponseHelper;
class ResponseHelper
{
public static function handle($view, $data)
{
if (request()->is('api/*')) {
return response()->json($data);
}
return view($view,$data);
}
}
And finally, a simple route:
Route::get('/products',[\App\Http\Controllers\ProductController::class,'index']);
// Or may be a post route in some cases
Route::post('/products',[\App\Http\Controllers\ProductController::class,'index']);
Our structure is now complete. Let's start with ACP requirements:
ACP Use Cases:
Below is sample code for the Admin Control Panel. In Blade views, $params are usually generated from form inputs or filter options.
// $params will be typically sent from blade view
// 1. Case: ACP paginated view
$params['scenario'] = 'admin';
// 2. Case: Filter by category_id
$params['category_id'] = 1;
// 3. Case: Filter by brand_id
$params['brand_id'] = 1;
// Case: may be apply sorting
$params['sortBy'] = 'stock';
$params['type'] = 'desc';
// Case: Exporting products to an Excel sheet
$params['scenario'] = 'export';
$params['paginate'] = false;
// Case: Product Inventory no params required
// Case: low stock products
$params['low_stock'] = true;
$params['scenario'] = 'admin';
// Case: out of stock products
$params['out_of_stock'] = true;
$params['scenario'] = 'admin';
// Case: expired products
$params['expired'] = true;
$params['scenario'] = 'admin';
EW Use Cases:
In ecommerce website you will fetch some parameters from database and pass them to this class.
// Case: trending products
$params['trending'] = true;
$params['scenario'] = 'compact';
// Case: new arrivals products. Do nothing query will send latest
// Case: show featured
$params['featured'] = true;
$params['scenario'] = 'compact';
// Case: keyword search
$params['keyword'] = 'ink pen';
$params['scenario'] = 'compact';
// Case: category search
$params['category_id'] = 1;
$params['scenario'] = 'compact';
// Case: apply sorting
$params['sortBy'] = 'price';
$params['type'] = 'asc';
$params['scenario'] = 'compact';
// Case: related products
$params['product_id'] = 1;
$params['scenario'] = 'compact';
MA Use Cases:
Mobile uses API, and all use cases are the same. The only difference is the ResponseHelper class will convert the response to JSON.
Final Thoughts
We’ve now built a complete AQC-based query flow using a single GetAllProductsclass. It can handle various use cases from admin panels, e-commerce websites, and mobile APIs—all from one consistent, reusable, testable place. By abstracting the query logic out of controllers and views, we’ve created a scalable and centralized structure that respects separation of concerns and makes future changes easier.
This is just the beginning. As your application grows, so will your needs. That’s when you can start splitting parts of the AQC class for better maintainability — let’s talk about that later.
I hope you enjoyed this post. I am open to getting criticism and suggestions. Feel free to drop a comment.
If you found this post helpful, consider supporting my work — it means a lot.
