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.
If you found this post helpful, consider supporting my work — it means a lot.
Top comments (19)
I seems to me you seen the CQRS pattern and you only took away separating each query in their own class.
While CQRS is over-engineering for most applications, there is a simpler pattern that has all the benefits mentioned in the post. And that is the repository pattern.
Basically all methods that address the same table in the database are grouped together. Like you would group CRUD methods in one controller.
The benefit of this pattern over AQC is that you can share code. Like the code for the
$scenario
argument in the examples.With the AQC pattern you need to create a trait to share the code. Which means instead of one class you need at least three.
The flaw with the more worked out
GetAllProducts
example seen in the third part of this series is that it should be another class. The reason is that it doesn't get all the products, just the ones that are filtered. That is atomic thinking for me.Thanks for the feedback. You’re right that AQC overlaps with CQRS and Repository, but the intent is different: instead of grouping everything in one repository (which can grow too big), AQC keeps each query as a self-contained unit and for CQRS it it does not force you to split read and write operations. That makes controllers/services depend only on the exact query they need and keeps tests very focused. For small apps Repository is fine, but in larger apps with many variations, I’ve found AQC keeps things cleaner.
And for the trait part if you are confused with naming convention, you can change them according to what suits you. Take
GetAllProducts
for example. Change it toGetProducts
to better reflect to what it does and you are good to go.If you group everything in one repository, you are doing the repository pattern wrong.
It does force you to split read and write operations because the pattern singles out each query. If you meant splitting read an write databases, CQRS doesn't care because is depends on the connection string you assign to the operations.
You should test the queries as standalone. Only testing the queries in the context of a controller or a service isn't a good test, there is too much going on in those methods certainly if is a controller method.
How does it keep things cleaner?
The trait has nothing to do with naming. From this sentence it seems you don't understand what a trait is.
It is a bad idea to use more generic names to fit a broader functionality of the query.
At some point calling the abstraction is going to be almost as much code as calling the model, which means the abstraction has not enough added value.
Thanks again for engaging. I think there may have been a misunderstanding about what AQC is aiming for.
With AQC, I don’t create a new class for every variant of a query (like GetActiveProducts, GetStoreProducts, GetProductsWithStock, etc.). Instead, I define a single atomic query per intent — for example:
GetProduct --> always used to fetch a single product.
GetProducts --> always used to fetch multiple products.
All variations/conditions (active, in-stock, by store, etc.) are applied dynamically based on parameters passed in inside that one class. This way, the application always calls the same entry point for that operation, and the query logic stays consistent in one place.
That’s the main difference from Repository: in a repository you often end up with many different methods per entity other than the basic ones, keeping or violating repository pattern class grow big and big, while in AQC you stick to one atomic query per case and make it flexible with parameters.
For me, that’s how it keeps things cleaner in larger codebases — there’s one and only one class to fetch multiple products and that is GetProducts class to look at when you need to understand or update how products are queried.
So that way of working in a repository would look like this
The methods of a repository are not defined. You can make them as broad or as narrow as you like. It all depends on what is easiest for the application.
That's exactly I have come to ths AQC pattern. This shows the key difference in philosophy between Repository and AQC.
With Repository, you group multiple methods in one class per entity, and that can work well for smaller apps. But in my experience, as the application grows, the repository either ends up very broad (with dozens of methods like
getActiveProducts
,getProductsByStore
, etc.), or stays too generic (likegetMultiple($config)
) where each usage has to reinvent the filter logic in the controller/service.With AQC, I want to avoid both extremes. Instead of an all-in-one repository, I define one query class per intent:
GetProduct --> always the way to fetch a single product (with dynamic conditions passed in).
GetProducts--> always the way to fetch multiple products (with dynamic conditions passed in).
That gives me a single place to look at when I need to understand or update how products are retrieved. The variations live inside the query class (not scattered across different repository methods or controllers), which keeps things consistent. Here's the example again.
So while Repository is flexible, I’ve found AQC to be more maintainable in large projects because every query has its own clear home, and controllers/services depend only on the exact atomic query they need.
I hope this clears things up and you see my point.
This is your example in the repository form
So I don't see the benefit for single table queries.
The case were I see the benefit of a separate class is when a query needs data from multiple tables. I seen classes where people added the UseCase suffix to separate them from the repository classes.
I think the best way is a mix of both.
Good point — and you’re right that for many teams, repositories handle straightforward single-table queries just fine. Where AQC really starts to shine, though, is when a project grows and even single-entity queries become more varied.
Take your repository example:
In a repository, you’ll likely end up creating multiple methods (getActiveProducts, getStoreProducts, searchProducts, etc.) or keep stacking more and more branching logic in one generic method. Either way, the class grows and mixes responsibilities and finally becomes a God-class.
With AQC, there’s only one GetProducts class with a single handle() method. All conditions (active, by store, by keyword, etc.) are expressed dynamically as parameters. That gives the application one clear entry point for "fetching multiple products" and all variations stay inside that unit.
So while Repository is flexible, AQC keeps things cleaner in the long run by avoiding both too many methods and over-generic methods. Moreover, when you need to call any variant, you just need to pass the right parameters and you are good to go. This will save you trouble to navigate through many methods. instead you will know exactly there is only one class responsible for fetching products.
Isn't that the same as
Why does the logic needs to be added, by adding more parameters, right?
For the one pattern it is a benefit and for the other a problem? That doesn't make sense to me.
How does a repository mixes responsibilities? A repository abstracts the data storage retrieval/manipulation.
Why a god class? i don't know which repositories you have seen, but I never saw a repository god class.
I never had a problem finding the right method in a repository.
You’ve raised three points, so let me address them one by one.
In a repository, if we take an e-commerce example, you often end up with multiple methods for different scenarios:
getProductsByCategory()
getProductsByKeyword()
getProductsByFeature()
And in every one of those methods, you’ll also repeat the same condition (e.g., fetch only active products). That leads to duplication means writing same where condition for each method.
In AQC, I only write that condition once inside the handle() method of GetProducts, and all variations (category, keyword, feature, etc.) are handled dynamically through parameters. So instead of repeating condition across methods, it’s centralized in one place. When sent the right parameter you avoid repeatition.
In practice, repositories are rarely used just for plain CRUD. Again taking the e-commerce example, I might need to:
In a repository, you’ll end up many new methods for each of these use cases. Over time, the repository grows into dozens of methods, each with overlapping logic. That’s what I mean by mixing responsibilities and turning into a God-class.
In AQC, I only have GetProducts. All the scenarios above are handled by checking parameters inside that one query class. This keeps the logic consistent and avoids method sprawl.
That’s fair — with modern IDEs like VS Code and others, finding methods is easier. But with AQC, there’s no guessing at all. If I or any teammate need to modify product fetch logic, we always know exactly where to look:
GetProducts
for multiple fetch.That consistency saves time in larger teams and larger projects.
So, while Repository and AQC both have their strengths, I’ve found AQC avoids code duplication, prevents repositories from growing too large, and makes the entry point for each query completely predictable.
We are going in circles at the moment.
I just showed you how you can create the
handle
method as a repository method,getMultiple
. And now you argue that repositories only can have specific methods.When I showed you the handle method in the repository, you wrote it is too generic.
If the method is too generic in the repository, it is also too generic in your pattern. You can't have a cake and eat it too.
As I mentioned before the
$argument
code is duplicated in theGetproduct
andGetProducts
classes.It seems you are so invested in the pattern that you don't recognize your logic fallacies.
Yes you are right. We are going in circles. Please wait for my new article. AQC vs Repository Design Pattern.
@xwero please read the 4th article of this series. i hope this clears many confusions.
dev.to/raheelshan/repository-patte...
The big problem with that post is you are assuming things that don't necessarily have to be true.
It is not because you write it often it becomes true.
First of all the naming of the method is wrong.
The bigger problem in that sentence is that you seem to think a class always needs be small. The class is as large or as small as it needs to be.
As a side note a god class has nothing to do with the size of the class. Please check your terminology before you use it.
I think there is the base problem I have overseen in our conversation.
Your
getProducts
class is mainly a very specified wrapper for the model builder pattern.Why would you go through all that effort just to end up with something that already is provided for you?
The flexibility you think your pattern has comes from the model builder pattern, not from your pattern.
Really think what the added value is of an abstraction before you start to use it. This is true for all design patterns you want to apply in your codebase.
Of course, I am not saying that the repository pattern always leads to bloated classes. What I am pointing out are the common scenarios where developers fall into that trap—creating method after method in repositories when they shouldn’t. While working with .Net Framework i have seen most of the time people create Helper or DAL classes and do this.
I agree with you that not every method in a repository has to carry multiple conditions. But my concern is that when the repository is used for all CRUD operations and developers start stacking conditions and variants, the class becomes too large and takes on responsibilities it shouldn’t. At this point it should be broken into multiple classes instead of being a
Repository
.When I said “small class,” I didn’t mean it literally has to be short. What I meant is: once a class is doing too much work, becoming harder to navigate, and carrying more responsibilities than it should—that’s where the problem lies. Ideally, we should aim for clarity: a class with a single responsibility, broken into smaller private methods, exposing only one public entry point.
Now, about
GetProducts
being a wrapper for the model builder—you’re absolutely right. It is a wrapper. But I deliberately do that for two reasons:Centralization
– Instead of calling queries directly from controllers, helpers, or even (accidentally) in views, I prefer to have one single source of truth. This way, any product-related query logic lives in one place and is reusable across the application.
Reusability and consistency
– By collecting query logic into AQC classes, I avoid scattering small queries throughout the codebase. That makes the overall system easier to maintain and extend.
You also mentioned that the flexibility I highlight really comes from the underlying model builder and not from AQC itself. I don’t fully agree. While I built AQC in Laravel for demonstration purposes, the pattern itself is aimed to be used in any framework. In fact, ORMs like Eloquent or others achieve the same idea: they construct queries dynamically based on methods like
where()
,whereIn()
,whereRaw()
. AQC brings that same principle but in a way that I can control and extend independently using parameters.Take a look at this same code.
So even if we remove Laravel’s builder or any ORM from the picture and write raw SQL, the same parameter-driven query construction is possible through AQC. That’s where I see the added value: it’s not tied to Eloquent’s builder but rather to the concept of query construction itself.
Lastly, I understand you’re viewing this within the Laravel ecosystem, but my experience extends across .NET, Python, and PHP. In different frameworks, I have repeatedly seen teams fall into the trap of stacking methods inside repositories. That’s why AQC is intended for a general audience, not just for Laravel developers.
If a class takes on responsibilities it shouldn't breaking up the class will not solve the problem.
This also doesn't prevent bad code. No pattern prevents bad code.
The point I wanted to make with my previous comment is that if the application has a query builder pattern build-in you already have a reusable centralized way to create queries.
No need to add a wrapper with custom logic.
Now we are getting somewhere. I agree that when the application doesn't include a query builder pattern, having methods that make it easier to create a query can be useful.
But I would go for a query builder pattern over AQC because with the builder pattern you don't need to change the builder code. With AQC you need to change the code for each field that is removed, changed or added.
I use the Laravel code because that is what you are using.
For .NET there is Linq, In Python there is SQLAlchemy core. In PHP you can use Doctrine.
People that are aware of common design patterns don't need your pattern to create a maintainable codebase. And because they are using a common design pattern other people will pick it up faster.
I'm not against thinking outside of the box, but the solution should provide value in a way other solutions can't.
AQC is really about discipline. Eloquent, Doctrine, SQLAlchemy, or LINQ give you the building blocks, but they don’t provide a project-wide convention for how query logic should be shared and reused. That’s where AQC adds value—it doesn’t replace the builder, it organizes and centralizes how your application interacts with it.
I’ve worked across .NET, Python, and PHP, and in all ecosystems I’ve seen teams fall into the same trap: query snippets scattered across controllers, helpers, and services. The ORM doesn’t prevent that. AQC is meant as a discipline layer on top, keeping queries reusable, predictable, and testable.
I agree common patterns are easier to pick up—but they also come with common pitfalls. AQC is my attempt at addressing those pitfalls. It’s not reinventing the wheel, but putting a guardrail on how the wheel is used.
How don't they provide project-wide conventions? They have an API.
The query logic you are mentioning are things like
If that is correct, just using the query builder methods prevent the need to write that logic. That is what I wanted to show with the snippet in my previous comment.
Do you really think people are not going to write bad code with your pattern? You are underestimating them.

I see your point—but let me clarify I don’t mean the ORM’s API. APIs give you building blocks, but they don’t dictate where query logic belongs or how it should be reused across features. That’s where we see differences.
The above example of conditional
if
is only to understand what philosophy leads to AQC idea.Product::where('active', true)->get()
is a hard-coded query. You already decided the filter at the call site. The real problem is when active is optional. The moment filters are optional, you need branching somewhere. So you didn’t remove the if; you just hid it in the controller/service:You’re right no pattern can stop someone from writing bad code. AQC is a discipline layer to keep query logic centralized, reusable, and testable. Developers can still misuse it, but it reduces the surface area for that misuse.
When in real world only a few follow disciplines. AQC is for them. Cheers.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.