Problem
Every service in the platform was reinventing ad-hoc filtering: optional request parameters glued into query strings, inconsistent pagination, and tenant scoping that depended on every developer remembering to add it. Hand-rolled filters were injection-prone and behaved differently on every endpoint.
Business Context
This library was extracted from EquipOS after the same filtering code appeared — with subtle differences and subtle bugs — in a fourth service. The goal was to make expressive client-side querying safe by construction: if the query compiles, it is parameterized, field-whitelisted, and tenant-scoped. There is no unsafe path.
Architecture
A classic three-stage pipeline:
query string → Lexer → Parser → AST → Validator → Compiler (SQL | Elasticsearch) → paginated result
The tokenizer and recursive-descent parser produce an AST. A validator checks every field reference against a whitelist and injects the tenant predicate. Pluggable compilers emit parameterized Spring Data queries or Elasticsearch query bodies — never concatenated strings.
Technology
Java 17, Spring Data, no runtime dependencies beyond Spring itself. The parser is hand-written rather than generated: for a small grammar, a recursive-descent parser is readable, debuggable, and dependency-free. ANTLR was the alternative and would have been the right call for a larger grammar. Parser and compiler stages have 100% test coverage — they are the security boundary.
Challenges
The core challenge was designing an ergonomic grammar. Early versions were expressive but hard to write by hand; the final grammar trades a little power for queries a human can type into a search box. The second challenge was keeping parse and compile stages strictly separated so new backends (a GraphQL compiler is on the roadmap) need zero parser changes.
Solution
Teams expose one search endpoint per resource, declare a field whitelist, and get consistent filtering, sorting, and cursor pagination. Tenant scoping is enforced at compile time, not by convention.
Results
- Zero injection surface — only parameterized queries can be emitted.
- Consistent search behavior across every service that adopted it.
- Removed hundreds of lines of duplicated, risky filtering code per service.
Lessons Learned
A small grammar plus an AST removes a whole class of bugs. Making the safe path the only path beats documentation and code review every time.