Skip to content

Instantly share code, notes, and snippets.

@alpharder
Last active October 16, 2025 21:34
Show Gist options
  • Save alpharder/46841456c5dc284bf73ef2a95d34c325 to your computer and use it in GitHub Desktop.
Save alpharder/46841456c5dc284bf73ef2a95d34c325 to your computer and use it in GitHub Desktop.
On ORMs

Business logic entities ARE NOT ORM models/entities

Important note: This paragraph doesn't attempt to describe a "silver bullet" aka "the only right way of doing things". What it describes is just a preferred approach of isolating persistence details from business logic at Tesseract. The world won't collapse if domain service makes direct calls to ORM or SQL. For some (usually small) projects, the separation of concerns described below may not be beneficial, and only add complexity and cognitive load instead.

Domain layer should know nothing about any persistence layer.

Domain logic entities' structure should not be dictated by your database tables structure.

These structures (domain and DB) may be similar or even identical sometimes, but should never be tightly coupled from domain layer's perspective.

Persistence (infrastructure, in our case) layer is aware of domain entities and exists to handle their persistence, and it's his responsibility to know how to do that in a most effective way taking into account real-world storage details, limitations and .

This additional level of abstraction of course brings additional complexity, but is practical and its benefits often overweight the downsides.

It's often misrepresented with the idea of "because what if you'll want to migrate from MySQL to Postgres?".

In practice, however:

  • this happens super rarely;
  • modern ORMs/query builders do usually encapsulate specific RDBMS details;
  • tight coupling of business logic code to RDBMS-specific details would be your least problem compared to data migration itself, service downtime and incompatibilities of how RDBMS' are working with your data internally;

What really makes this abstraction worth it:

  • Tight coupling of business logic entities/types to DB structure may limit possibilities of database:
    • table structure changes;
    • query optimization;
    • denormalization;
    • sharding and partitioning;
    • The real question that often raises during the product growth is "what if we'll store this subset of data at Redis/Dragonfly/... instead?". Without proper separation of persistence layer this kind of migration would leak more infrastructure-specific code into your business logic codebase.
  • Your initial implementation used JSONB column to store subset of data, and now you want more performance by extracting some of its subfields into table columns? Or do you need to use a JSONB column instead of that 1:1 relation? Good luck modifying half of your business logic code!

Of course, this doesn't mean that business logic doesn't actually depend on what's in your database. At the end of a day, your database's primary purpose is to serve business logic needs.

Business logic entities should be structured in a most effective way to write business logic code. Persistence layer should implement the most effective way of persisting business logic entities.

Oversimplifying stated above – it's just the code that handles business logic should not depend on the abstractions provided by the database-related code.

When writing code, think of your domain model first, leaving persistence out of question, but keeping its related concerns in mind for overall vision of the system.

You generally don't want to design a domain model that's impossible to be effectively persisted.

You generally don't want to design domain model around persistence mechanism rather than functional and correctness requirements.

ORMs should not be a default choice

ORMs are great, until they aren't.

These writings are worth reading in order to familiarize yourself with a different angle of treating ORMs:

So ORMs aren't always great, what should we use instead?

Writing raw SQL can also be problematic (I do not agree with the last part of this article though, because adding another meta-language abstraction doesn't seem to be a great idea, as demonstrated by https://www.prisma.io/).

There are some newer developments in TypeScript ORMs – for example Orchid ORM and PureORM, which aim to solve some of the typical ORM problems.

However, at the time of writing those aren't battle-tested solutions and need to be cooked more prior to be eligible of being advised to use at commercial projects.

Remember, ORM is a pattern and the value of a pattern is not the pattern itself, but the idea behind it.

Assuming that we've encapsulated database-related code in a persistence (infrastructure) layer and this abstraction doesn't leak much into business logic code, [as described above](#Business logic entities ARE NOT ORM models/entities"), what do we really want from this abstraction?

  • Typesafety
  • SQL injection prevention
  • Avoiding writing boilerplate code
  • Populating relation data

All of these features are provided by an excellent Zapatos library, which Tesseract encourages you to use within infrastructure layers of your moduliths.

It has downsides too:

  • Only supports Postgres
  • Doesn't provide runtime type-safety

An alternative to that could be Kysely, which supports more RDBMS dialects, but requires more hand-coding for populating relation data and doesn't provide runtime type-safety too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment