The Best Abstractions Do Not Hide Structure: They Expose Leverage
Hatched by Kai Nguyen
Aug 24, 2026
10 min read
3 views
93%
What if the most expensive mistake in software design is not writing too much code, but hiding the wrong information?
A class can be beautifully organized and still make a system rigid. A SQL query can return the correct rows and still force a database to examine millions of irrelevant ones. In both cases, the failure is deeper than poor style or missing optimization. The system has been given an instruction whose meaning is technically valid but operationally difficult to exploit.
This reveals a powerful connection between object oriented design and database performance: good abstractions preserve the information that downstream systems need in order to make intelligent decisions.
In application code, that downstream system may be another developer, a testing framework, or a future feature. In a database, it is the query planner and the indexes it can use. The vocabulary differs, but the design problem is the same. We want to express intent without destroying leverage.
The hidden cost of an opaque instruction
Consider a simple database query. Suppose a table contains an indexed created_at column, and we want all records from the last year. A programmer might write:
WHERE YEAR(created_at) = 2025
This is understandable to a human. But the database may need to calculate YEAR for every row before deciding which rows qualify. The index on created_at cannot be used as effectively because the column has been wrapped in a function.
A more useful expression states the same intent as a range:
WHERE created_at >= '2025-01-01'
AND created_at < '2026-01-01'
The second query is not merely faster because it contains fewer characters or follows a clever trick. It is faster because it exposes the structure of the search. The database can navigate directly to the relevant region of the index.
This is the essence of a SARGable condition: a predicate expressed in a form that allows the database to use its search machinery. The difference is between asking, “Which rows become true after I transform every value?” and asking, “Where is the interval of values I need?”
The same distinction appears in software architecture. Imagine a payment service that accepts a single, highly general method:
process(request, options, context, flags)
It may appear flexible. In practice, callers must understand a large collection of implicit rules. Which flags are compatible? Which fields are required for each payment type? What happens if a context object is absent? The method has hidden the structure of the problem inside a bag of parameters.
A design guided by the single responsibility principle might separate the responsibilities more clearly:
charge_card(card, amount)
refund_card(transaction_id, amount)
record_payment(payment)
This interface is less universal, but more useful. Its shape tells callers what it does, makes invalid combinations harder to express, and gives tests a smaller surface to cover.
In both examples, clarity is not cosmetic. Clarity determines whether the next layer can optimize, validate, replace, or reason about what it has received.
An abstraction succeeds when it compresses complexity without compressing away the structure required for good decisions.
SOLID and SARGability solve the same architectural problem
The five SOLID principles are often taught as rules for arranging classes. Query optimization is usually taught as a matter of indexes, execution order, and database mechanics. Their shared insight is more general: systems perform best when intent is expressed in a form that preserves independent reasoning.
Take the open closed principle. A component should be open to extension but closed to modification. The practical concern is avoiding a central block of logic that must be repeatedly edited as new cases arrive. A query can suffer from a similar form of entanglement when it applies functions, negations, calculations, sorting, grouping, and filtering in ways that obscure the basic restriction being made.
Suppose an indexed status column is queried like this:
WHERE NOT status = 'cancelled'
Depending on the database and data distribution, this may be less useful to the optimizer than a positive, selective condition. If the real business requirement is a known set of active states, this can be clearer and more exploitable:
WHERE status IN ('pending', 'paid', 'shipped')
The second form does more than avoid negation. It states the permitted domain directly. It is analogous to replacing a large conditional branch with a set of explicit strategies or types. The system can see the cases instead of inferring them from an exclusion.
The dependency inversion principle offers another parallel. High level policy should not depend directly on low level details. In application code, that often means a service depends on a narrow repository interface rather than on a specific database driver. But dependency inversion is not an excuse to create an abstract layer that erases all useful distinctions.
A repository method named find_relevant_items() may protect the service from SQL syntax, but it can also conceal whether the operation is a selective indexed lookup, a full table scan, or an expensive sort. The abstraction has inverted a dependency while hiding a performance contract.
A better interface might communicate the important dimensions:
find_orders_by_customer(customer_id, limit=50)
The method does not expose SQL implementation details. It does expose the access pattern: a lookup by customer, with a bounded result set. That information helps developers reason about performance and encourages the implementation to remain aligned with the intended query shape.
This suggests a refinement to the usual understanding of abstraction:
An abstraction should hide implementation details, not operational consequences.
A caller should not need to know whether an index is implemented as a B tree or another structure. But the caller may need to know whether requesting ten thousand records is materially different from requesting fifty. A service should not care whether a query uses one particular database library. But its contract should make clear whether it promises stable ordering, pagination, or bounded work.
The optimizer problem exists in human teams too
Database engines are not the only systems that optimize based on visible structure. Human teams do this constantly.
A small, cohesive class allows a developer to predict where a change belongs. A narrow interface allows a tester to construct focused cases. A query with an early WHERE clause allows the database to discard irrelevant rows before sorting or grouping. In each case, early reduction lowers the amount of work that later stages must perform.
This leads to a useful mental model: every system has a cost pipeline.
At each stage, the system receives information, transforms it, and passes a result onward. If irrelevant possibilities are eliminated early, downstream work shrinks. If distinctions are blurred early, later stages must recover them through more computation, more branching, or more scanning.
In a web application, the pipeline might look like this:
- Validate the request.
- Identify the narrow use case.
- Apply authorization.
- Select the required data.
- Transform the result.
- Render the response.
If authorization is delayed until after a large data load, the system has performed expensive work before eliminating forbidden possibilities. If every request enters a giant generic handler, the system has delayed classification and forced later code to interpret ambiguous inputs.
SQL has an analogous pipeline. Filtering early with WHERE, selecting only needed columns, limiting rows, and avoiding unnecessary sorting or grouping all reduce the work passed to subsequent operations. The database execution order may not match the written order of a query, but the design objective remains: make the cheap, selective decisions available as early as possible.
This is why the single responsibility principle has a performance dimension, even when it is presented as a maintainability principle. A component with one clear reason to change is easier to place in the pipeline. It can reject irrelevant work, delegate a focused operation, and be optimized independently.
The reverse is also true. A poorly designed query is often a symptom of poorly separated business responsibilities. If one endpoint simultaneously searches, calculates eligibility, applies pricing rules, sorts by several dynamic fields, and formats a report, the SQL becomes difficult to optimize because the application has not decided what the operation actually is.
Architectural ambiguity eventually becomes computational work.
The danger of abstraction without a performance contract
There is a fashionable failure mode in both object oriented programming and database access layers: abstraction is treated as valuable simply because it adds distance from implementation.
A generic data access method might accept arbitrary filters:
query(entity='order', filters=filters, sort=sort, include=includes)
This can reduce repetitive code at first. Over time, it becomes a miniature query language with unclear guarantees. Callers can request leading wildcard searches, unbounded results, multiple joins, and expensive sorts without realizing the consequences. The abstraction has made it easier to issue bad queries.
The same problem appears in a class hierarchy built around a broad base class. If every subclass inherits methods it does not meaningfully support, the interface lies. The substitution principle is violated in spirit: an object may technically fit the type but behave unexpectedly under certain operations.
A narrower design creates productive constraints. For example:
class OrderSearch:
def by_customer(self, customer_id, limit=50):
...
def by_created_range(self, start, end, limit=50):
...
The interface does not support every imaginable query. That is a feature. It makes common access paths explicit, encourages indexed predicates, and forces unusual searches to receive deliberate treatment instead of quietly becoming production bottlenecks.
The lesson is not that generic abstractions are always bad. Genericity is valuable when the underlying operations share a stable structure. A reusable pagination component is sensible when every endpoint follows the same contract. A universal filter object is dangerous when it permits combinations with radically different cost profiles.
A practical test is to ask three questions:
- What decisions can this abstraction make visible?
- What expensive behavior can it accidentally permit?
- Which guarantees should be part of the interface rather than left to convention?
For a class, the guarantees might concern side effects, valid inputs, and substitutability. For a data access method, they might concern indexable fields, maximum result size, ordering, and consistency. In both cases, the interface is not merely a doorway. It is a policy about what kinds of work are normal.
Designing for the next decision
The deepest connection between maintainable classes and efficient queries is not “keep things simple.” Simplicity is too vague. The more precise principle is this:
Design every boundary so the next decision can be made with the least necessary work and the most relevant information.
This principle changes how we review code.
When reviewing a class, do not ask only whether its methods are short. Ask whether each method reveals one coherent decision. Can a caller understand what it needs to provide? Can a test isolate the behavior? Can an implementation change without forcing unrelated code to change?
When reviewing a query, do not ask only whether it returns the right result. Ask whether the predicate exposes a searchable shape. Can an index narrow the candidates? Are functions applied to the indexed column? Is a leading wildcard forcing a scan? Are sorting, grouping, and calculations happening before the result set has been reduced?
When reviewing an architecture, ask where ambiguity is being paid for. Is it paid once at the boundary, where the request is classified and validated? Or is it paid repeatedly by every downstream component that must interpret a generic object, inspect flags, and guess which behavior is intended?
A strong system pays for interpretation early and then carries explicit meaning forward. It converts vague requests into narrow operations. It turns broad possibilities into bounded sets. It preserves the information that enables both machines and humans to choose efficient paths.
Key Takeaways
- Treat interfaces as optimization surfaces. A class or repository method should expose the shape of the work, not merely conceal its implementation.
- Prefer searchable and explicit expressions. In SQL, use range predicates, positive conditions, appropriate indexes, early filters, and bounded result sets. In application code, use focused methods and explicit use cases.
- Hide mechanics, not consequences. Callers need not know database internals, but they should understand limits, ordering, consistency, and likely cost.
- Eliminate ambiguity early. Validate, authorize, classify, and narrow requests before expensive computation or broad data access begins.
- Be suspicious of universal abstractions. Reuse is valuable when behavior is genuinely shared. A generic interface that permits uncontrolled combinations may convert design problems into runtime cost.
The best design is not the one that hides the most. It is the one that hides accidental complexity while preserving essential structure.
That is why a clean class and a fast query are closer relatives than they first appear. Both are acts of translation. They take a human intention and express it in a form that another system can execute intelligently. When the translation preserves leverage, the result is software that is easier to change and cheaper to run. When it destroys structure, no amount of polish can fully recover what was lost.
The question for every abstraction, then, is not simply, “Is this elegant?” It is more demanding: “What useful decision will become impossible, expensive, or invisible because I designed it this way?”
Sources
Hatch New Ideas with Glasp AI 🐣
Glasp AI allows you to hatch new ideas based on your curated content. Let's curate and create with Glasp AI :)
Start Hatching 🐣