There is a class of project where the ORM is exactly the right tool: small, conventional, schema-stable, with one team and one deploy target. Almost none of the engagements we take are in that class.
On the work we do — trading systems, clinical platforms, multi-tenant operating systems — the database is the load-bearing wall. We write the schema by hand, the migrations by hand, and the queries by hand. The ORM, when it is in the codebase at all, is reserved for CRUD on the boring tables.
A small example
A typical hand-written migration on one of our projects looks like this. Idempotent, narrow, with a comment that names the on-call engineer who ran it.
-- 2026-02-04 · added partial index for active orders
-- non-blocking deploy; backfill measured on staging first
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_active
ON orders (account_id, created_at DESC)
WHERE status IN ('open', 'partial');Three useful things happen when you write the migration this way. The query planner becomes legible. The on-call engineer can read the index name and know what it is for. And the migration becomes reviewable as code, not as a side effect of an annotation in a model file three directories away.
The ORM is wonderful for the day you write the query. Postgres is wonderful for the next eight years.
We are not doctrinaire. Two of our active engagements are running on Prisma; one is on SQLAlchemy. But the heart of every system we ship is a hand-written schema with hand-written queries on the hot paths, and on those paths we measure twice and index once.
