skip to content

A Repository interface exposes methods like findActiveCustomersInRegion(region) instead of letting callers build SQL WHERE clauses themselves. What is this technique called, and what problem does it prevent?

level: middleimportance: must knowfreq 65%

answer

  1. intention-revealing method names
  2. no raw SQL escapes to callers
  3. prevents SQL injection at the boundary
  4. Specification/Criteria for combinatorial filters
  5. one place to change the query

basics

~20 s

It's called query encapsulation: the Repository hides the actual query logic behind a named method, so callers just say what they want, not how to fetch it, and can't accidentally write unsafe or database-specific queries.

solid answer

~40 s

This is query encapsulation — the Repository exposes intention-revealing methods (findActiveCustomersInRegion) that internally build whatever query the storage technology needs, rather than exposing a generic 'run this query' escape hatch. It prevents callers from embedding raw SQL/HQL fragments into business code, which would reintroduce the coupling the Repository was meant to remove, and it prevents SQL injection risk since parameters are passed as typed method arguments rather than string-concatenated. It also gives you one place to change a query's implementation (add an index hint, switch to a different join strategy, cache the result) without touching every call site. The trade-off is method proliferation: as query needs grow, teams often move to a Specification or Criteria object to keep the interface from exploding into dozens of narrowly-named methods.

go deeper

for a junior

Should recognize that named query methods are safer and clearer than passing raw SQL fragments into business code.

for a middle

Should explain both benefits — reduced coupling and SQL-injection prevention — and know that Spring Data-style derived queries are a real-world instance of this.

for a senior

Should know how to handle combinatorial filter requirements via Specification/Criteria without breaking encapsulation, and can spot when a 'generic query' escape hatch has crept back in.

for a principal

Should be able to weigh the encapsulation-vs-flexibility trade-off at a system level — e.g., deciding whether a reporting subsystem needs a dedicated read-query layer outside the Repository altogether rather than stretching Specifications past their useful complexity.

## What the technique is Query encapsulation means the Repository interface exposes **methods named after what the caller wants** (`findActiveCustomersInRegion(region)`, `findOverdueInvoices()`, `findByEmail(email)`) rather than exposing a generic mechanism for callers to build arbitrary queries themselves (e.g., a method that accepts a raw SQL string, or an open-ended `find(String whereClause)`). The method signature and name communicate the business intent; the query logic that satisfies it — a SQL `WHERE` clause, a MongoDB filter document, an in-memory `Stream.filter()` — lives entirely inside the implementation, invisible to the caller. Two callers asking `findActiveCustomersInRegion("EU")` get the same guaranteed behavior no matter which concrete Repository implementation is wired in. The method's parameter list is also part of the encapsulation: `region` arrives as a typed value the implementation controls how to bind, not as a fragment of text the caller assembled by hand. ## The problem it solves The problem this solves has two faces. 1. **Coupling.** If callers were allowed to pass raw query fragments, every call site would need to know the underlying schema and query dialect, and any schema change (renaming a column, changing a join) would force edits across the codebase instead of inside one Repository implementation. Query encapsulation keeps that knowledge in exactly one place. 2. **Second, security** — and just as important in production systems. Allowing callers to construct query strings (especially by string concatenation with user input) is the textbook opening for SQL injection. By accepting typed method parameters (`region: String`, `since: Instant`) and having the implementation bind them as query parameters — not string-interpolate them — the Repository closes off an entire class of injection vulnerabilities at the architecture level, not just by code-review vigilance. This is a **structural guarantee rather than a discipline-dependent one**: a reviewer doesn't need to re-verify every call site for correct escaping, because the interface's shape makes the unsafe path unreachable in the first place. ## The trade-off The trade-off is that a fully encapsulated, method-per-query Repository doesn't scale well as query variety grows. A reporting screen with five independently toggleable filters (status, region, date range, tag, assignee) would need 2^5 combinations if you tried to give each combination its own named method, which is obviously unworkable. Real systems handle this with a middle ground: a **Specification or Criteria object** (as in Spring Data's `JpaSpecificationExecutor`, or a hand-rolled `CustomerQuery` builder) that lets callers compose filter predicates through a typed, safe API without falling back to raw strings. This preserves encapsulation — callers still can't inject arbitrary SQL — while avoiding combinatorial method explosion. The cost is a more complex Repository implementation and a slightly leakier abstraction, since the specification objects usually mirror the query capabilities of the underlying store fairly closely. ## Failure modes Failure modes show up in a few recognizable ways. - Teams that resist adding a Specification mechanism sometimes cave to pressure by adding a **generic `findByCriteria(Map<String, Object> filters)` escape hatch**, which silently reintroduces the coupling and validation problems the pattern was meant to prevent — now any caller can pass arbitrary keys the implementation must defensively interpret, and typos become runtime bugs instead of compile errors. - Another common failure is **method names that don't actually match their query semantics after a refactor** — `findActiveCustomersInRegion` that quietly starts including recently-deactivated customers because someone changed the underlying filter without renaming the method, breaking the intention-revealing contract callers relied on. - A third failure mode is **performance**: an encapsulated method can hide an expensive full-table scan behind an innocent-looking name, and because callers can't see the query, they have no signal to know they should be worried about calling it in a loop (the classic N+1 problem when a `findById` is called per-item instead of a single `findByIdIn(ids)`). - A fourth, quieter failure is **interface bloat that outlives its usefulness**: methods added for a single one-off report five years ago linger in the interface indefinitely because nobody is confident enough to delete a public method they can't prove is unused, and the Repository slowly becomes a junk drawer of narrowly-scoped queries. ## A real-world instance A concrete real-world instance: Spring Data JPA's derived query methods let you declare `findByStatusAndRegion(Status status, String region)` directly as an interface method with no method body — Spring parses the method name at startup and generates the corresponding JPQL query automatically, which is query encapsulation taken to its logical extreme (the method signature *is* the query specification). When query needs outgrow what a derived method name can express, the same library offers `@Query` with named parameters, or `Specification<T>` composition, as the escape valves — illustrating exactly the encapsulation-versus-flexibility trade-off in a widely used production framework.

  • How does a Specification or Criteria object avoid the method-explosion problem while still counting as query encapsulation?
    A Specification composes small, typed predicate objects (e.g., `hasStatus(ACTIVE).and(inRegion("EU"))`) through a fixed, safe API rather than accepting raw strings, so callers can express arbitrary combinations of filters without the Repository interface needing a distinct named method for every combination. It stays encapsulated because callers still can't inject arbitrary query text — they can only compose from predicates the Repository's domain vocabulary explicitly exposes.
  • Why is binding query parameters (rather than string-concatenating them) part of what makes this pattern secure?
    Parameter binding sends the query structure and the data separately to the database driver, so user-supplied values are always treated as data values, never as executable query syntax, which is what prevents SQL injection. String concatenation collapses that separation, letting malicious input like `' OR '1'='1` change the query's meaning; a Repository whose derived or `@Query` methods bind typed parameters closes this off by construction rather than relying on every caller remembering to sanitize input.

It's like ordering from a restaurant menu instead of walking into the kitchen: you say 'the salmon, medium rare' (a named method) and the kitchen figures out the recipe; you never hand the chef a raw list of ingredients and cooking steps (a raw query string) to execute blindly.

saying these in an interview costs you the question

  • Suggests exposing a generic 'runQuery(String sql)' method on the Repository as the solution to flexible querying
  • Doesn't mention SQL injection when asked why raw query strings shouldn't cross the Repository boundary
  • Thinks every possible filter combination needs its own named method with no awareness of Specification/Criteria patterns
  • Can't explain what changes at the call site when a query's implementation changes internally

context