Blog/Three abstractions

Three abstractions and nothing else

There is a kind of framework complexity that gets mistaken for power. If you cannot explain the model on a whiteboard, that is not richness. It is surface area you will be debugging later. ST/ORM has three user-facing concepts, and they are enough to explain the model.

February 17, 2026Design4 min read

Entity

The data your application works with. A Kotlin data class or a Java record with a couple of annotations, @PK for the primary key and @FK for a foreign key. ST/ORM connects entities to tables and columns by convention, so annotations show up only where the convention does not fit. An entity carries no hidden state and no behavior. It is database-backed data as a plain value, and nothing else.

Repository

CRUD operations and type-safe queries for one entity. You define an interface and write the query method bodies with the DSL. There is no method-name parsing that turns findByStatusAndCreatedAtAfter into a hidden query, and no generated query you have to reverse-engineer from a log. If a repository runs a query, you wrote it, and you can read it.

SQL Template

Direct access to SQL with type-safe binding and result mapping, for when the DSL is not the right tool: joins, reports, database-specific features, or queries where SQL should stay SQL. It is a first-class citizen that sits beside the DSL, not a trapdoor you fall through when the abstraction runs out. Type references and metamodel columns keep it checked; parameters are bound automatically. It follows the case for keeping SQL in view rather than hiding it.

Why a small model is the point

There is more underneath, of course: transactions, a static metamodel, change detection, generated code. It is all there to support these three, not to add a fourth concept you have to learn.

These three share one principle: visible behavior over framework decisions you have to guess later. No query fires that you did not write, and the SQL each one produces is predictable. Every relationship is loaded when you ask for it. Every transaction boundary is declared, not inferred. The payoff is not just aesthetic. It is that you can teach the whole model to a new engineer in an afternoon, and never have to reconstruct what the framework decided to do on your behalf. A model you can hold in your head is easier to teach, easier to review, and harder for the framework to surprise you with later.