Queries as Contracts: How CTEs and Interfaces Stop Complexity From Eating Your Project

Kai Nguyen

Hatched by Kai Nguyen

Apr 16, 2026

8 min read

82%

0

A small, dangerous truth

What if the reason your codebase feels like a jungle is the same reason your analytics queries are inscrutable? Most teams treat database queries and object design as separate disciplines. One is declarative and tabular, the other is procedural and class based. In practice they are faces of the same problem: how to make messy systems understandable, testable, and composable. If you learn to think of queries as interfaces and interfaces as queries, you gain a simple, repeatable discipline that prevents complexity from spreading like mold.

Start with a surprising observation: naming a subquery can do for SQL what declaring an interface does for a codebase. Both acts turn an amorphous process into a readable contract. Both enforce expectations, reduce duplication, and make reasoning about change tractable.

This article will show how those parallels are not metaphors only. They are practical design patterns you can apply today to tame data logic and program logic together.


The tension: flexibility that becomes fragility

There are two powerful tendencies in software and analytics. One is abstraction: we create blueprints that say what should be done, not how. In code this looks like an interface that declares methods with no implementation. In queries this looks like a Common Table Expression declared with the WITH clause, which names a subquery that can be referenced by a larger query.

The other tendency is immediacy: when something works, we reach for the quickest change. We copy a join, paste a filter, add a window function like RANK, and the result works for the urgent ticket. That immediacy is seductive because it yields fast answers. Over time it yields chaos. Small inconsistencies proliferate into hard to detect logic errors. Your team spends hours hunting where a particular business rule lurks.

Why does this happen? Because both queries and classes are ways of coordinating intent between people who are not in the room. If the intent is only embodied in raw code or raw clauses, it is easy for future contributors to violate or duplicate it without noticing.

Two concrete patterns illustrate the failure modes:

  1. Copy pasted filters and joins across many queries, adjusted slightly in each place to 'fit' a report. These slight adjustments yield divergent definitions of the same business concept.

  2. Informal, undocumented interfaces in Python: a class that signals how it is to be used by duck typing, but with no enforced contract. When the codebase grows, methods drift, names change, and logic breaks in subtle ways.

Both failures share a root cause: insufficiently explicit contracts between components.


Synthesis: an interface-query duality and a simple mental model

The key insight is to treat the act of naming as design. Naming a subquery with WITH and naming a method in an interface are both contracts. They do not by themselves provide the full implementation. They do something more valuable: they make expectations explicit and localize the rules.

Think of a mental model called the Interface Query Duality. It has three axes:

  • Declaration vs Implementation: An interface or a CTE declares shape, column names, or method signatures. The implementation lives somewhere else: a concrete class implements the method, a query body implements the CTE.

  • Composition vs Mutation: Interfaces encourage composition through polymorphism. CTEs allow composition of query logic by referencing named subqueries. Both discourage mutation of the declared contract; changing a contract requires an explicit act that affects all implementers.

  • Scope and Visibility: A declared interface is part of a module boundary. A named CTE is scoped to a query. The naming makes it easier to reason about visibility and the surface area that can change without breaking consumers.

These axes yield a practical framework for design. When you build a feature that touches data or code, apply the same sequence of steps whether you are writing SQL or Python:

  1. Name the contract. In SQL use a CTE with a clear name that expresses the concept. In Python define an abstract base class or an informal interface class with method names that read like promises.

  2. Declare the shape. For SQL this means the column names and the meaning of each column. For Python this means the method signatures and their documented return types or behavioral expectations.

  3. Implement the contract separately. Create the concrete query or the concrete class that fulfills the contract. Keep the implementation local to a module or to a query when possible.

  4. Test against the contract. Write tests that exercise the contract boundary only. Tests that assume internal implementation details will break when you refactor. Tests that verify contracts will survive implementation changes.

This framework maps directly to concrete practices that reduce accidental complexity.


Concrete examples that make the pattern tangible

Example 1: A CTE as an interface

Imagine you need customers with their lifetime value and an eligibility flag. You could fold everything into a single long query. Or you can name the concept.

WITH customer_base AS (
  SELECT
    customer_id,
    SUM(order_total) AS lifetime_value,
    MAX(order_date) AS last_order_date
  FROM orders
  GROUP BY customer_id
),

eligible_customers AS (
  SELECT
    customer_id,
    lifetime_value,
    CASE WHEN lifetime_value > 1000 THEN true ELSE false END AS eligible
  FROM customer_base
)

SELECT * FROM eligible_customers
WHERE eligible = true
ORDER BY lifetime_value DESC

The CTE customer_base declares the shape and intent. downstream logic can rely on lifetime_value being present and defined the same way across queries. If you ever need to change how lifetime value is computed, you change it in one place and all consumers of the CTE remain consistent.

Example 2: An abstract interface in Python that reads like a CTE

Open a file contracts.py and write:

from abc import ABC, abstractmethod

class CustomerValueProvider(ABC):

    @abstractmethod
    def get_lifetime_value(self, customer_id):
        """Return the lifetime value for the given customer id as a number."""
        pass

Now any concrete provider must implement get_lifetime_value. Tests can rely on that method signature and behavior. The contract reads like documentation and prevents accidental divergence of method names or expectations.

Example 3: Window functions as behavioral mixins

Window functions such as RANK or ROW_NUMBER let you compute attributes across partitions without collapsing rows. Think of them as mixins that add a behavior across groups. If you partition by customer_id and order by order_date you compute positional information for each order while preserving the row shape.

That is similar to adding a method to an interface that computes a derived behavior without changing the underlying data shape. The core idea is separation of shape and behavior.

Example 4: Virtual base classes and plugin registries

A virtual base class that uses subclasshook and .register() lets you register implementations that are not direct subclasses but still conform to the contract. This is useful for legacy classes or for implementations in other modules where you cannot alter the base class hierarchy. The pattern mirrors query materialization strategies, where a view or materialized table can be registered as the canonical implementation of a named contract.


Practical rules you can apply today

The interface-query duality is more than a metaphor. It yields immediately usable practices for both analysts and engineers.

Rule 1: Name before you optimize. When designing a report or API, first write a named CTE or an abstract method that captures the high level concept. Only then worry about performance or micro optimizations.

Rule 2: Keep contracts thin and intention revealing. A contract should be a promise, not an encyclopedia. For SQL name the intent and the columns. For Python keep the interface methods minimal and document their behavior.

Rule 3: Use the language features that enforce contracts when you need scale. Small projects can get away with duck typing and inline subqueries. As the project grows, adopt ABCMeta, subclasshook, or register virtual subclasses. In SQL adopt consistently named CTEs or view definitions that teams agree on.

Rule 4: Treat window functions as composable behaviors. When you need positional or ranked attributes, compute them as a separate named step. That makes it easier to change ranking rules without reworking joins or aggregates.

Rule 5: Test against the contract boundary. Write tests that call the interface method or run the query that consumes the CTE output. Avoid testing internal implementation details that you plan to refactor.


Key Takeaways

  • Use naming as design: create CTEs or interfaces to declare the shape and intent before implementing.
  • Prefer explicit contracts in code and queries when teams grow; start informal when small and promote formality when needed.
  • Separate shape from behavior: compute aggregates and ranking in named steps; declare methods that return primitives and then compose behaviors on top.
  • Apply the same testing approach across data and code: assert on the contract surface only, not on implementation internals.
  • When integration is required, use registration or view definitions to provide canonical implementations without tight coupling.

Naming a subquery or declaring a method is not paperwork. It is a discipline that converts tacit knowledge into a testable, composable contract.

A final reframing

Most people treat SQL and object design as tools that solve distinct problems. The pragmatic truth is that they solve the same challenge in different idioms: how to coordinate human intent across time and teams. Declaring an interface or a named subquery is a simple act that buys you future freedom. It is cheap in the moment and priceless later.

When you leave the small pleasures of quick fixes for the discipline of naming contracts, you will notice two immediate benefits. First, refactoring becomes safer because you change an implementation and your tests exercise the promise, not the internals. Second, collaboration becomes easier because your teammates can read a named contract and understand what is expected.

So next time you reach for a quick inline join or a throwaway method name, pause. Ask whether a short contract would make the interaction clearer. The code you write under that constraint will be slightly longer in the immediacy but far more durable over time. That is the ultimate payoff: systems that remain understandable when the team changes and when deadlines multiply.

Think of every CTE and every abstract method as a footprint you leave for the future. Make those footprints intentional, clear, and dignified. Your future self and your team will thank you.

Sources

← Back to Library

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 🐣