Backend Development Atomic Query Construction (AQC) Software Architecture Clean Code

Working with Relational Data in Atomic Query Construction Design Pattern

Muhammad Raheel

Muhammad Raheel

March 15th, 2026 · 8 min read

Working with Relational Data in Atomic Query Construction Design Pattern

In the previous parts of this series, I introduced the Atomic Query Construction (AQC) design pattern and how it helps organize data access in a structured and predictable way.

So far, everything looked clean because we were dealing with single models. No relationships.

Today I am going to talk about how we can achieve relational data in the AQC design pattern, and this is the final article in this series.

The Problem with Traditional Relationship Handling

In a typical Laravel application, relational queries are written wherever they are needed:

User::with('profiles')->where(...)->get();

This shows up in:

  • controllers
  • services
  • random helper classes
  • sometimes even Blade (because why not ruin everything)

At first, it works and feels flexible.

Over time, it becomes a mess:

  • Different developers load different relationships
  • Constraints vary across the codebase
  • Some queries forget eager loading entirely

You no longer have a single definition of what a “User with Profiles” actually means.

AQC Approach to Relationships

AQC doesn’t allow this flexibility. Instead, AQC enforces a simple rule:

Each model owns its data access logic, including its relationships.

So:

  • GetUsers handles users
  • if profiles are needed, GetUsers handles that internally
  • No external layer decides how relationships are loaded
class GetUsers
{
    public function handle(array $params = [])
    {
        $query = User::query();

        $this->filters($query, $params);
        $this->relationships($query, $params);

        return $query->get();
    }

    private function relationships($query, $params)
    {
        if (!empty($params['with_profiles'])) {
            $query->with('profiles');
        }
    }
}

Controller stays clean:

GetUsers::handle([
    'with_profiles' => true
]);

One entry point. One place to reason about the query. No query logic outside.

The Rule That Makes This Work

This is where AQC becomes strict:

AQC classes do not call other AQC classes.

Even for relationships. So this will never happen:

GetUsers → calls → GetProfiles

Instead:

  • GetUsers directly handles profiles
  • GetProfiles is only used when profiles are the primary concern

Why This Rule Matters

Allowing AQC classes to call each other introduces:

  • hidden dependencies
  • execution flow becomes harder to track
  • accidental complexity
  • debugging becomes annoying

By preventing this, AQC ensures:

  • Predictability – One class, one query flow
  • Clarity – No hidden orchestration
  • Control—Relationships are always explicit

Handling Complexity Internally

Queries will grow. Filters will increase. Conditions will pile up.

AQC doesn’t stop that. It just forces all of it into one place.

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

        $this->applyFilters($query, $params);
        $this->applyRelationships($query, $params);

        return $query->get();
    }

    private function applyFilters($query, $params)
    {
        if (!empty($params['active'])) {
            $query->where('is_active', true);
        }
    }

    private function applyRelationships($query, $params)
    {
        if (!empty($params['with_profiles'])) {
            $query->with('profiles');
        }
    }
}

Instead of spreading logic across the application, everything is:

  • grouped
  • readable
  • controlled

Yes, the class grows, but only in one place, and that’s the trade.

What About Reuse?

AQC intentionally avoids cross-class reuse in queries.

Yes, this can lead to duplication, but that’s okay.

Because AQC prioritizes:

  • clarity over cleverness
  • ownership over abstraction

If you’re going:

  • User → Profile → handled in GetUsers
  • Profile → User → handled in GetProfiles

Each direction has its own entry point.

Testing AQC Queries

Since all query logic is centralized, testing becomes more focused and straightforward.

AQC classes can expose query debugging:

$query->toSql();

This helps:

  • inspect generated SQL
  • validate conditions
  • debug complex filters

However, testing should not rely only on SQL strings.

You should also test:

  • returned data
  • applied conditions
  • edge cases

What This Pattern Actually Does

AQC doesn’t reduce complexity.

It just stops it from spreading everywhere.

Instead of:

  • queries in multiple layers
  • inconsistent relationship handling
  • random eager loading

You get:

  • one entry point per model
  • controlled relationships
  • predictable queries

Final Thought

This approach is strict on purpose. And it works because it forces discipline. Break the rules, and you’re back to the same mess you were trying to fix. It is not the most flexible approach. It is not the most reusable approach. But it is consistent, controlled, and maintainable.

Comments


Comment created and will be displayed once approved.