DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • ANEW Can Help With Road Safety
  • Required Capabilities in Self-Navigating Vehicle-Processing Architectures
  • The AI Autonomy Spectrum: 7 Architecture Patterns for Intelligent Applications
  • Engineering Closed-Loop Graph-RAG Systems, Part 2: From Prompts to Rules

Trending

  • Reactive Ops to Autonomous Infrastructure: How Agentic AI Is Redefining Modern DevOps
  • Why Your RAG Pipeline Will Fail Without an MCP Server
  • Top JavaScript/TypeScript Gen AI Frameworks for 2026
  • Beyond Conversation: Mastering Context with Claude Code Skills and Agents
  1. DZone
  2. Coding
  3. Languages
  4. Querying Without a Query Language

Querying Without a Query Language

Imagine querying your domain using simple attribute filters, without building queries or learning a query language, and getting the result back as JSON.

By 
Jan Nilsson user avatar
Jan Nilsson
·
May. 21, 26 · Tutorial
Likes (0)
Comment
Save
Tweet
Share
1.1K Views

Join the DZone community and get the full member experience.

Join For Free

Most backend systems don’t really have a query model.
Not one that has been designed, at least.

Querying usually starts small, we filter on a field, maybe two. The ORM makes it easy, and there’s no real reason to think about it. It does what we need.

Then something changes.

We need one more filter.
Then another.
Then we need to filter on something that isn’t on the root.

That’s where it starts to feel different.

Filtering on a related entity is no longer just a condition. It brings in joins, affects what is returned, and starts to interact with pagination and result structure in ways that aren’t obvious anymore.

At that point, we don’t really describe queries. We build them.

We combine predicates, add joins, shape the result, and try to keep the behavior consistent. The logic lives in code and understanding what a query actually does requires reading how it is constructed.

These problems don't exist on day one, they accumulate, they grow.

And still, this is rarely something we question.
It’s just how querying works.

But what if it didn’t have to?

A Simpler Way to Express a Query

What if querying didn’t require constructing queries at all?
What if the only thing the client needed to do was describe what to filter on?

In its simplest form, that could be reduced to three things:

  • an attribute
  • an operator
  • a value

For example:

  • title EQ "Math"
  • name EQ "Anna"

Nothing more.

No query language.
No Criteria API.
No building of predicates.

Just attributes from the domain and a small, fixed set of operators such as EQ, GT, LT, or IN.

A query starts from a part of the domain.
If we are working with courses, we start with a course.
If we want to filter on students related to those courses, we add a student filter as part of that structure.

There is no need to describe how to join or navigate between entities. The structure of the query follows the structure of the domain itself.

From the client’s perspective, querying becomes a matter of choosing attributes and applying filters, not constructing queries.

This is a strong restriction.

It does not allow arbitrary expressions.
It does not try to cover every possible case.

But that is intentional.

The goal is not to make querying more powerful.
The goal is to make it easier to understand.

When Filtering on a Child

Filtering on a root attribute is straightforward.
If we want courses with a specific title, the query can be expressed like this:

JSON
 
{
"course": {
   "title": "Math",
   "titleOp": "EQ"
   }
}


Nothing unusual. The situation changes when we want to filter on something that belongs to a related part of the domain.

For example:
Courses where a student’s name is "Anna". In many systems, this is where query logic starts to grow.

Filtering on a related entity means:

  • adding joins
  • adding conditions on the joined data 
  • making sure the result still represents courses 
  • handling duplicates
  • and making pagination behave correctly

Even for a simple condition, the query is no longer simple.
In this model, the structure changes but the expression does not.

The query still consists of:

  • attributes
  • operators
  • values

We just place the filter where it belongs in the domain:

JSON
 
{
"course": {},
   "students": {
   "name": "Anna",
   "nameOp": "EQ"
   }
}


This works because the structure is already known.

The system knows what a course is.
It knows that a course has students.
It knows how those parts of the domain relate to each other.

That knowledge comes from the domain model, the same model used to define entities and their relationships.

This pattern emerged from a system where the domain is described as metadata and made available to the system.

The query does not need to describe how to move between these parts.
It only needs to point to where the filter applies.

This also defines what should be returned.

  • Only courses that have at least one matching student are returned 
  • Within those courses, only the students that match the filter are included 

There is no ambiguity about whether the filter affects selection or result shape. It does both, in a consistent way.
From the client’s perspective, nothing has changed.

The query is still just:

  • attribute
  • operator
  • value

The only difference is where the filter is placed.
All complexity such as joins, execution and result handling is moved into the system.

Where Does the Complexity Go?

Up to this point, the query has stayed simple.

  • attributes
  • operators
  • values
  • structured according to the domain

But the underlying problem has not disappeared.
Filtering on students still requires:

  • joining related data
  • selecting the correct courses
  • returning the correct students
  • and making sure pagination behaves correctly 

That complexity still exists; it just has moved.

Instead of being constructed in the client or in application code, it is handled by the system. The system already knows the structure of the domain.
It knows how courses and students are related, and how those relationships are represented as entities.

Based on that, it takes responsibility for:

  • determining how to navigate between parts of the domain 
  • selecting the correct root objects
  • applying filters in the right place 
  • and shaping the result so that it matches the query 

The client does not describe how this should be done.
It only describes what should be filtered.

This introduces a clear trade-off.

The query model becomes deliberately simple and constrained.
In return, the system becomes responsible for handling the complexity that was previously spread across query code.

That responsibility is not trivial.

It means the system must be able to:

  • interpret the structure of the query 
  • understand how filters affect both selection and result 
  • and execute the query in a way that produces consistent results 

One important consequence of this is that queries can no longer be executed as a single step. Handling this consistently requires a different way of executing them.

The Two Phase Query

The query is executed in two phases.

In the first phase, the system determines which root objects match the query.

This includes applying filters on related parts of the domain.
For example, when filtering on students, the system determines which courses have at least one matching student.

The result of this phase is a set of matching root objects.

In the second phase, the system builds the result.

It loads the selected root objects and adds the related data that should be returned.
If a relation is filtered, only matching objects are included.
If it is not filtered, all related objects are included.

This separation is important.

Filtering determines which root objects should be returned.
Result construction determines what those objects contain.

Keeping these concerns separate makes it possible to:

  • apply filters without breaking the structure of the result 
  • return only the relevant parts of related data 
  • keep pagination stable 

From the client’s perspective, none of this is visible.

The query is still just:

  • attributes
  • operators
  • values

The complexity is still there.
 It is just handled in one place instead of many.

A Deliberately Limited Model

This is not a general-purpose query model.

It does not try to support every possible query.
It does not aim to replace all forms of querying in a system.

The model is intentionally limited.

It focuses on a specific kind of problem:
querying across a known domain structure using simple, explicit filters.

That means certain things are deliberately left out:
There is no support for arbitrary query expressions.
There is no attempt to model complex boolean logic.
There is no built-in way to express every possible combination of conditions.

These are not missing features.
They are conscious decisions.

The purpose of the model is not to maximize flexibility. It is to make the common case easy to express and easy to understand.

Most queries in a system follow predictable patterns:

  • filter on attributes
  • filter across known relationships
  • return data in the shape of the domain 

For those cases, a simple and consistent model is often enough.

When queries move beyond that, they should be written explicitly.
Application-specific queries, complex conditions, aggregations, or optimizations belong in code where they can be controlled and understood.

This model does not try to absorb those cases, it defines a default path, not the only path.

By keeping the model limited, it remains predictable. The client knows what it can express and the system knows how to interpret it. The behavior remains consistent across different parts of the application.

Closing

Querying is often treated as something we build.
We combine filters, construct conditions, and rely on tools to turn that into something that works.

This approach starts from a different assumption.

That querying is something we can design.
Not to make it more powerful, but to make it easier to understand.

This pattern allows you to query your domain using simple, standard operators such as EQ, GT, and IN, without constructing queries or learning a query language. The result follows the structure of the domain and remains consistent across different queries.

This pattern is not theoretical. It emerged from a system where the domain is described as metadata and made available to the system.

Query language Filter (software) systems

Opinions expressed by DZone contributors are their own.

Related

  • ANEW Can Help With Road Safety
  • Required Capabilities in Self-Navigating Vehicle-Processing Architectures
  • The AI Autonomy Spectrum: 7 Architecture Patterns for Intelligent Applications
  • Engineering Closed-Loop Graph-RAG Systems, Part 2: From Prompts to Rules

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook