DEV Community

Cover image for Introducing The Atomic Query Construction (AQC) Design Pattern
Raheel Shan
Raheel Shan

Posted on • Originally published at raheelshan.com

Introducing The Atomic Query Construction (AQC) Design Pattern

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/
Enter fullscreen mode Exit fullscreen mode
  • 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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Usage:

// Admin listing
$products = GetAllProducts::handle(); 
// return paginated product order id latest 

// Frontend product listing
$products = GetAllProducts::handle($request->all(), true, 'compact');
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Usage:

// Admin edit screen
$product = GetProduct::handle($productID, [], 'admin');

// Frontend product detail
$product = GetProduct::handle($productID, [], 'compact');
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (19)

Collapse
 
xwero profile image
david duymelinck • Edited

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.

Collapse
 
raheelshan profile image
Raheel Shan

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 to GetProducts to better reflect to what it does and you are good to go.

Collapse
 
xwero profile image
david duymelinck

grouping everything in one repository

If you group everything in one repository, you are doing the repository pattern wrong.

it does not force you to split read and write operations

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.

controllers/services depend only on the exact query they need and keeps tests very focused

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.

in larger apps with many variations, I’ve found AQC keeps things cleaner.

How does it keep things cleaner?

And for the trait part if you are confused with naming convention

The trait has nothing to do with naming. From this sentence it seems you don't understand what a trait is.

Take GetAllProducts for example. Change it to GetProducts to better reflect to what it does

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.

Thread Thread
 
raheelshan profile image
Raheel Shan • Edited

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.

Thread Thread
 
xwero profile image
david duymelinck

So that way of working in a repository would look like this

class ProductRepository 
{
     public function getOne(int $id): Product|null
     {
         // code here
      }

      public function getMultiple(array $config): array|null
      {
         // code here
      }
}
Enter fullscreen mode Exit fullscreen mode

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.

Thread Thread
 
raheelshan profile image
Raheel Shan • Edited

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 (like getMultiple($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.

class GetProducts
{
    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

       // select columns

        // return results
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Thread Thread
 
xwero profile image
david duymelinck

This is your example in the repository form

class ProductRepository
{
    public static function getMultiple($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

       // select columns

        // return results
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Thread Thread
 
raheelshan profile image
Raheel Shan

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:

  • How do you get active products but not store products?
  • How do you get store products either active or inactive?
  • How do you search products by keyword?

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.

Thread Thread
 
xwero profile image
david duymelinck

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.

Isn't that the same as

keep stacking more and more branching logic in one generic method

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.

the class grows and mixes responsibilities and finally becomes a God-class

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.

This will save you trouble to navigate through many methods

I never had a problem finding the right method in a repository.

Thread Thread
 
raheelshan profile image
Raheel Shan • Edited

You’ve raised three points, so let me address them one by one.

Keep stacking more and more branching logic in one generic method.

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.

// apply only when requested
if (isset($params['active'])) {
    $productObj->where('active', $params['active']);
}

// apply only when requested
if (isset($params['store_id'])) {
    $productObj->where('store_id', $params['store_id']);
}

// apply only when requested
if (isset($params['store_id'])) {
    $productObj->where('store_id', $params['store_id']);
}

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

// apply only when requested
if (isset($params['brand_id']) && $params['brand_id'] > 0) {
    $productObj->where('brand_id', $params['brand_id']);
}
Enter fullscreen mode Exit fullscreen mode

The class grows and mixes responsibilities and finally becomes a God class.

In practice, repositories are rarely used just for plain CRUD. Again taking the e-commerce example, I might need to:

  • fetch related products for the product detail page
  • list products on a category page
  • apply filters (brand, price range, attributes)
  • support sorting (by price, by name)
  • allow searching by keyword

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.

I never had a problem finding the right method in a repository

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.

Thread Thread
 
xwero profile image
david duymelinck

We are going in circles at the moment.

you often end up with multiple methods for different scenarios

In a repository, you’ll end up many new methods for each of these use cases

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.

AQC avoids code duplication

As I mentioned before the $argument code is duplicated in the Getproduct and GetProducts classes.

It seems you are so invested in the pattern that you don't recognize your logic fallacies.

Thread Thread
 
raheelshan profile image
Raheel Shan • Edited

Yes you are right. We are going in circles. Please wait for my new article. AQC vs Repository Design Pattern.

Collapse
 
raheelshan profile image
Raheel Shan • Edited

@xwero please read the 4th article of this series. i hope this clears many confusions.

dev.to/raheelshan/repository-patte...

Collapse
 
xwero profile image
david duymelinck

The big problem with that post is you are assuming things that don't necessarily have to be true.

People start stacking method after method in the same class

If developers want to stick to original 5 methods of repository pattern, they push logic into services, helpers, and other classes

So parameters act as decision maker for creating new methods

In a repository, you’d have to write a new method for each of those combinations, a nightmare.

It is not because you write it often it becomes true.

Similiar to the findAll() other methods can have the same where conditions making the class go large.

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.

But when we do add one, we add it once and immediately unlock all combinations.

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?

GetProducts::handle(['active' => true]);
//
Product::where('active' => true)->get();
Enter fullscreen mode Exit fullscreen mode

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.

Collapse
 
raheelshan profile image
Raheel Shan • Edited

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:

  1. 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.

  2. 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.

$sql= 'SELECT * FROM products WHERE 1 = 1 ';

// apply only when requested
if (isset($params['active'])) {
    $sql.= " AND active = 1";
}

// apply only when requested
if (isset($params['store_id'])) {
    $sql.= " AND store_id = " . $params['store_id'];
}

// apply only when requested
if (isset($params['category_id'])) {
    $sql.= " AND category_id = " . $params['category_id'];
}

$conn = new mysqli($servername, $username, $password, $dbname);
// all done. now execute query here
$result = $conn->query($sql);
Enter fullscreen mode Exit fullscreen mode

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.

Thread Thread
 
xwero profile image
david duymelinck

the class becomes too large and takes on responsibilities it shouldn’t. At this point it should be brken into multiple classes instead of being a Repository.

If a class takes on responsibilities it shouldn't breaking up the class will not solve the problem.

exposing only one public entry point.

This also doesn't prevent bad code. No pattern prevents bad code.

This way, any product-related query logic lives in one place and is reusable across the application.

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.

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.

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 understand you’re viewing this within the Laravel ecosystem, but my experience extends across .NET, Python, and PHP

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.

Thread Thread
 
raheelshan profile image
Raheel Shan

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.

Thread Thread
 
xwero profile image
david duymelinck

but they don’t provide a project-wide convention for how query logic should be shared and reused

How don't they provide project-wide conventions? They have an API.

The query logic you are mentioning are things like

if (isset($params['active'])) {
    $sql.= " AND active = 1";
}
Enter fullscreen mode Exit fullscreen mode

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.

// This requires an if in the method
GetProducts::handle(['active' => true]);
// This doesn't need parameter testing
Product::where('active' => true)->get();
Enter fullscreen mode Exit fullscreen mode

AQC is meant as a discipline layer on top

Do you really think people are not going to write bad code with your pattern? You are underestimating them.
desgin versus user meme

Thread Thread
 
raheelshan profile image
Raheel Shan • Edited

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.