The History of ORM: Between Objects and Relational Databases
Everyone has a plausible idea at first.
Prologue...
My codebase has a comment like this.
// TODO: fix this laterIt was written three years ago. For me, "later" is a fairly broad concept.
Most software starts this way: as a temporary workaround. You write SQL directly, map rows to objects, then convert those objects back into INSERT or UPDATE statements. At first, it seems like nothing more than a bit of repetitive work.
But keep writing the same code long enough, and your thinking starts to shift.
"Do I really have to do this by hand every single time?"
ORM (Object-Relational Mapping) was born from exactly that question. It was not some magic invented to eliminate SQL; it was an attempt to reduce the recurring translation overhead that arises when an object-oriented application needs to persist state in a Relational Database (RDBMS).
Application developers want to work with objects. Database-side developers and the databases themselves want to work with rows, columns, keys, joins, and transactions.
Both sides have a point, which is precisely why they are always at each other's throats. And as is always the case, the person left cleaning up this grand clash of paradigms is my three-years-ago self, the one who left behind the comment "TODO: fix this later." I still haven't fixed it, and experience tells me I never will. The industry calls this a "stable architecture."

Future users of large data banks must be protected from having to know how the data is organized in the machine (the internal representation). A prompting service which supplies such information is not a satisfactory solution. Activities of users at terminals and most application programs should remain unaffected when the internal representation of data is changed and even when some aspects of the external representation are changed. Changes in data representation will often be needed as a result of changes in query, update, and report traffic and natural growth in the types of stored information.
Existing noninferential, formatted data systems provide users with tree-structured files or slightly more general network models of the data. In Section 1, inadequacies of these models are discussed. A model based on n-ary relations, a normal form for data base relations, and the concept of a universal data sublanguage are introduced. In Section 2, certain operations on relations (other than logical inference) are discussed and applied to the problems of redundancy and consistency in the user’s model
-E. F. Codd, “A Relational Model of Data for Large Shared Data Banks,” Communications of the ACM, Vol. 13, No. 6, pp. 377–387, 1970. DOI: 10.1145/362384.362685.
1. Let there be RDBMS!
In 1970, E. F. Codd proposed the relational model in "A Relational Model of Data for Large Shared Data Banks."1
The core idea can be summarized as follows.
Data is represented in table-like structures.
It is organized into rows and columns.
Integrity is maintained through relationships and constraints.
It separates the physical storage mechanism from the logical query mechanism.
As IBM explains, what Codd wanted was to allow users to work with information without needing to know the physical internal structure of the database.2 Taken at face value, this sounds no different from the soulmate of every office worker: Microsoft Excel. In fact, the intuition behind both is identical: organize data into a two-dimensional table, filter it, and leave it in a state someone else can open later.
But a relational database is not simply "a spreadsheet floating around in a shared folder." If Excel is a loose table designed for human eyes, a relational database is a system that enforces strict rules on top of that table: keys, constraints, Joins, Transactions, and concurrency control.
The idea was powerful. Relational databases quickly became the absolute standard for business systems and enterprise data.
The reason is simple: data never exists for just one program. Data stored today gets read by the accounting system tomorrow, rendered on an admin dashboard, swept up by batch jobs every night, and eventually analyzed for statistics by a BI tool.
Individual applications come and go with the trends, but the database outlives them all as the intersection of every system. We rip out and replace the frontend and backend every three years, yet nobody dares touch the DB.
In a typical company, the food chain of the ecosystem goes something like this: code is outlived by data, and data is outlived by an Excel file named final_FINAL_realFINAL.xlsx. I wrote that as a joke, but sadly it isn't one.

A:So why are you here?
B:I use Excel as a database
A:Stay away from me

2. They Tried to Make a Match from the DB's Rib, but the Shapes Did Not Fit
Through the 1980s and 1990s, Object-Oriented Programming (OOP) went mainstream.
In the object world, concepts like User, Order, and Product feel natural. Objects carry both state and behavior. They reference other objects, mutate state through methods, and extend models through encapsulation and Inheritance.
var order = orderRepository.Find(orderId);
var city = order.Customer.Address.City;
order.Cancel();In object-oriented code, this flow feels natural: an order knows its customer, the customer knows their address, and the address has a city. Developers navigate and reason along the object graph. It is generally the kind of structure we find intuitive.
But a relational database does not think that way.
A DB has no object references. It has Foreign Keys. A DB has no object graph. It has Joins. A DB has no object lifecycle. It has Transactions, row versions, and constraints. A DB has no method calls. It has set operations and query plans.
Developers working in an object-oriented style typically want to start from "a single order" and follow related objects from there (and in practice, this really does make debugging easier).
order.Customer.Address.CityA DB, on the other hand, wants to compute "the set of rows it needs" all at once.
SELECT ...
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN addresses a ON a.customer_id = c.address_id
WHERE o.id = :orderId;Neither is wrong. They simply divide the world along different axes.
Object-Oriented Programming (OOP) typically builds its model around identity, reference, behavior, and encapsulation, while the relational model builds its model around relations, tuples, keys, constraints, and set operations.
This difference is the Object-Relational Impedance Mismatch.
Object-Oriented World | Relational Database World |
|---|---|
Object identity | Primary key |
Object reference | Foreign Key |
Object graph traversal | Join |
Method call |
|
Encapsulated state mutation | Row modification inside a Transaction |
Inheritance | Table-per-Hierarchy, Joined Table, Table-per-Concrete-Class |
Collections | Related table or join table |
Object lifecycle | Persistence state, row version, constraint |
Current in-memory state | Durable state committed to the DB |
Inheritance in particular is a poor fit for relational databases. In object-oriented programming, models like AdminUser : User or PremiumCustomer : Customer feel natural and even elegant, yet relational databases have no built-in concept of inheritance. As a result, an ORM must choose a mapping strategy such as table-per-hierarchy, joined table, or table-per-class.
What feels "natural" in the object world does not automatically translate into what feels "natural" in the database world.
ORM therefore emerged not as a tool that eliminates this gap, but as one that reduces the translation cost between the two worlds.
3. Early ORM: "Can't We Just Save the Object As-Is?"
During the 1990s, there were numerous attempts to store objects in databases.
On one side stood Object-Oriented Database Management Systems (OODBMS), built on the idea that objects should be stored as-is rather than shredded into tables.
However, OODBMS had its own weaknesses. The ODMG standard lacked broad adoption, ad-hoc querying and reporting were nowhere near as convenient as with relational systems, and vendor lock-in was significant. On top of that, the center of gravity for enterprise data had already shifted decisively toward relational databases. The SQL ecosystem, reporting tools, operational experience, DBA culture, transaction processing, standardization, and vendor product suites had all grown up around the relational model.
So the practical question shifted.
Let's use a new database that stores objects as-is.
gave way to:
How do we cram objects into the relational databases we already have?
TopLink is frequently cited as the archetype of the early commercial ORM. It originated in the Smalltalk world, then crossed over into Java, providing mapping between objects and relational data.3
Here is the key point.
ORM did not emerge from some abstract pursuit of elegance. Enterprises were already running relational databases, and application code was increasingly being written in an object-oriented style. The repetitive hand-mapping between the two worlds became unbearable, so developers decided to cram their objects into the databases they already had.
4. Three Ways to Cram Inheritance into Tables
Inheritance was the crown jewel of object-oriented programming in the 1990s and early 2000s. Relational databases, however, have no intelligence for understanding such a refined concept. To squeeze elegant inheritance trees into two-dimensional tables, ORM had to offer three rather awkward mapping strategies.
public class User
{
public long Id { get; set; }
public string Email { get; set; } = "";
}
public class AdminUser : User
{
public string AdminRole { get; set; } = "";
}
public class PremiumCustomer : User
{
public int LoyaltyPoint { get; set; }
}In code, AdminUser is a User and PremiumCustomer is a User. But relational databases have no built-in concept of this kind of inheritance structure. ORM therefore has to translate an object's inheritance hierarchy into a table structure.
There are three representative strategies.
4.1 Single Table Strategy
Table-per-Hierarchy, Single Table Inheritance
This approach places the entire inheritance tree in a single table. Every column from both parent and child classes is packed into one table, and a DTYPE or Discriminator column identifies the actual type.
Users
- Id
- Email
- DTYPE
- AdminRole
- LoyaltyPointTo put it plainly: you shove a regular user, an admin, and a VIP customer all into the same studio apartment, then tell them apart by the name tags pinned to their chests.
The advantage is straightforward: queries are fast. Because all data lives in a single table, no joins are needed when querying an inheritance hierarchy.
The problem is that the table grows increasingly awkward. The AdminRole column, needed only for AdminUser, means nothing to regular users or premium customers. LoyaltyPoint, needed only for PremiumCustomer, means nothing to administrators. The result is a table with many columns perpetually filled with NULL.
When the inheritance hierarchy is small and the differences in columns between types are not significant, this approach is practical. It's blunt but fast. The problem is that as the hierarchy grows, the table increasingly starts to resemble Swiss cheese.
Queries are simple and fast, but nullable columns multiply and the schema becomes messy.
4.2 Joined Table Strategy
Joined Table, Table-per-Subclass
This strategy maps the parent class and each child class to separate tables, with each child table referencing the parent table's primary key as a Foreign Key.
Users
- Id
- Email
AdminUsers
- Id (FK -> Users.Id)
- AdminRole
PremiumCustomers
- Id (FK -> Users.Id)
- LoyaltyPointPut another way, this is the approach a database would love most. Common attributes go in the parent table, while type-specific attributes go in each child table. From a Normalization standpoint, it's clean.
However, assembling a single object requires joining those tables back together. To fully read one AdminUser, you must join Users and AdminUsers.
SELECT *
FROM users u
JOIN admin_users a ON a.id = u.id
WHERE u.id = :id;As the inheritance hierarchy deepens, the number of joins grows. When there are many type-specific queries or a high volume of polymorphic queries spanning multiple child types, the cost increases significantly.
That said, more joins do not automatically mean deadlocks. Ordinary read joins are generally not a direct cause of deadlocks. The real concerns are join cost, execution plan complexity, index design, and the predictability of the queries ORM generates.
The schema is Normalized and clean, but queries require more joins and execution plans grow more complex.
4.3 Table-per-Concrete-Class Strategy
Table-per-Class, Table-per-Concrete-Class
Rather than creating a parent table, this strategy creates an independent table for each concrete class. The common columns from the parent class are copied into each child table.
AdminUsers
- Id
- Email
- AdminRole
PremiumCustomers
- Id
- Email
- LoyaltyPointIn plain terms, this strategy says: use inheritance in code, but let each class fend for itself in the database. Each concrete class owns its own table.
The problem begins the moment you query by a common parent type.
List<User> users = userRepository.FindAllUsers();From the database's perspective, there is no parent table called Users. That means every child table must be searched.
SELECT Id, Email, 'AdminUser' AS DTYPE
FROM admin_users
UNION ALL
SELECT Id, Email, 'PremiumCustomer' AS DTYPE
FROM premium_customers;When there are only a few child types, this is manageable. As the number of types grows, however, queries against the common type become increasingly expensive. Because common columns are duplicated in each table, schema changes are also cumbersome.
Per-type tables are straightforward, but common queries and polymorphic queries become difficult, and common columns end up duplicated.
Comparing the three strategies yields the following picture.
Strategy | Advantages | Disadvantages | Best suited for |
|---|---|---|---|
Single Table / Table-per-Hierarchy | Queries are simple and fast | Proliferation of nullable columns, bloated table | When differences between types are small and the inheritance hierarchy is shallow |
Joined Table / Table-per-Subclass | Clean from a Normalization standpoint | More joins on reads, more complex execution plans | When data integrity and schema clarity matter most |
Table-per-Class / Concrete Table | Each type's table is fully independent | Requires UNION, duplicates shared columns, and makes polymorphic queries difficult | When queries against the parent type are rare and each type is highly self-contained |
Ultimately, inheritance mapping in ORM is less about "choosing the right answer" and more about "deciding which cost you can live with."
Object-oriented programming tries to express concepts naturally through inheritance. Relational databases aim to retrieve data as sets, without redundancy, while enforcing constraints. What feels natural to each is fundamentally different. ORM cannot erase that gap; it simply lets you choose which side's inconvenience you are more willing to bear.
4.4 Why Modern Design Prefers Composition
Looking at inheritance mapping strategies, a thought naturally arises:
"Why not just avoid inheritance altogether?" That is precisely why modern object-oriented design often favors composition over inheritance. Instead of building deep type hierarchies like AdminUser : User, the approach is to model User as holding objects such as Role, Permission, Profile, and Subscription.
public class User
{
public long Id { get; set; }
public string Email { get; set; } = "";
public UserProfile Profile { get; set; } = new();
public List<Role> Roles { get; set; } = new();
public Subscription? Subscription { get; set; }
}Inheritance says "A is a B."
AdminUser is a User.
PremiumCustomer is a User.Composition says "A has a B."
User has Roles.
User has Profile.
User has Subscription.From the perspective of a relational database, composition is more natural than inheritance. Databases are inherently comfortable connecting tables to tables through keys.
users
- id
- email
user_profiles
- user_id
- display_name
- avatar_url
roles
- id
- name
user_roles
- user_id
- role_id
subscriptions
- user_id
- plan
- expires_atThis structure is less awkward from the database's perspective than inheritance mapping. You don't need a DTYPE column to distinguish types, and there's no need to split tables per subclass and then stitch them back together with UNION. In most cases, foreign key, join table, and unique constraint are sufficient to express the relationship.
But composition is not free either.
From the object's perspective, user.Profile looks like a natural ownership relationship, but from the database's perspective it's a separate table with a Foreign Key. user.Roles is an object collection, but in the database it's a join table like user_roles. user.Subscription looks like a nullable reference, but in the database it's a relationship where the row may or may not exist.
Composition also raises a new set of questions.
When you delete a
User, shouldUserProfilebe deleted along with it?Does
Rolebelong to aUser, or is it shared across multiple users?Is
Subscriptiona value object or an independent entity?Should Cascade Delete be enabled?
How do orphan rows get handled?
Where does the aggregate boundary end?
Within a single Transaction, how far does consistency need to be guaranteed?
Once you enter the world of Composition, a developer must implement one of two extreme pathological conditions into their system.
The first is 'hoarding disorder (orphan row neglect)': so afraid of CASCADE that nothing ever gets deleted. The decaying profile photos of users who left five years ago, already-expired empty subscriptions, and ownerless shopping carts pile up quietly in the corners of the database, aging in silence. Then one day, while peacefully running a LEFT JOIN, you stumble upon this data and realize in horror that you've been violating privacy law.
The second is 'pathological arson (CASCADE DELETE)': the moment a user clicks the account-deletion button to check out, the system diligently incinerate not just their room but also the carpet, the hallway, the staircase, and any other shopping cart data that was unfortunately linked to them. The point is, there are cases where data that should have been kept gets deleted too.
The amusing part is that in the past, pressing this arson button was called 'an architectural catastrophe,' but these days, thanks to Privacy Law, it's celebrated as admirable compliance. It's a fine example of the state granting legitimacy to one of the two pathologies.
In other words, Composition reduces the pain of inheritance mapping, but it does not eliminate the fundamental friction between the object world and the relational world. The problem simply shifted from "how do we shoehorn inheritance in" to "do we set fires or learn to live with the garbage." The one thing worth remembering is that setting the occasional fire tends to be better than carrying the garbage with you forever.
In practice, though, Composition is generally the better choice. Relational databases express relationships more naturally than inheritance, and in Object-Oriented Programming (OOP) as well, composing small objects tends to be more resilient to change than deep inheritance hierarchies.
So the consensus these days is roughly this:
Use Inheritance only when a type hierarchy genuinely exists and is stable in the domain; model most things with Composition and relationships instead.
The history of ORM taught us this lesson in a rather expensive way.
The phrase 'rather expensive way' is industry slang meaning that countless developers before us quietly packed their desks and quit after staring at exploding DB locks and N+1 Query Problem logs on their monitors. Thanks to their sacrifice, we can type has_many with a steady hand today.
5. The Era of Java Enterprise and EJB Entity Beans
EJB Entity Beans were introduced in EJB 1.0 (1998) and, by the time EJB 2.0 (2001) arrived, had solidified into the standard Persistence model for Java enterprise development.
The problem was how heavy they were.
There was an enormous amount of configuration, a heavy dependency on the container, and far too much ceremony for a developer to go through just to persist a single table.
The word "enterprise" sometimes functions as a strange get-out-of-jail-free card: complexity gets called "enterprise-grade," slowness gets called "robust," and painful APIs get called "standard." EJB Entity Beans are a good window into the atmosphere of that era.
Hibernate (the representative Java ORM) grew as a reaction against this.
Hibernate is a Java ORM created by Gavin King, and its core philosophy was to map ordinary Plain Old Java Objects (POJOs) to the database.
Developers wanted to work with regular classes rather than heavyweight objects tethered to a container. Hibernate bound object-state changes to database Transactions through features such as Lazy Loading, Dirty Checking, Unit of Work, and Identity Map.
From this point on, ORM ceased to be a simple row mapper and became a tool that managed the entire Persistence layer of an application.
6. Fowler Codifies the Language
In 2002, Martin Fowler's Patterns of Enterprise Application Architecture codified the pattern language surrounding ORM.4
The names introduced there still appear regularly when reading ORM documentation today.
Active Record
Data Mapper
Unit of Work
Identity Map
Lazy Load
Repository
These names matter because the ORM debate is not simply a question of whether to use SQL or not.
For example, Active Record is an approach where a single object takes responsibility for persisting itself.
user = User.find(1)
user.name = "Kim"
user.saveData Mapper, by contrast, separates the domain object from the responsibility of DB mapping.
The `User` object owns the domain rules.
The `UserMapper` or ORM infrastructure handles persistence to the database.Active Record is fast and simple, while Data Mapper is more complex but allows the domain model to be kept somewhat more independent from the database structure.
This choice is typically driven less by personal preference and more by the scale of the application.
Active Record works well for small CRUD apps. For systems with complex domains and long lifecycles, the Data Mapper family tends to be a better fit. That said, using Data Mapper in a complex system does not automatically produce sophisticated design; when you abstract away the messy work, you just make it more expensive.
7. Rails Active Record: Popularizing ORM
When Ruby on Rails arrived in 2004, the Active Record style became the common vocabulary of web development.
The official Rails documentation describes Active Record as the M in MVC, the layer responsible for representing data and business logic.5
The appeal of the Rails-style ORM was clear.
class User < ApplicationRecord
has_many :orders
endAlign your table names, column names, and relationship names with the conventions, and a great deal of configuration simply disappears. This is convention over configuration.
For early web services, admin dashboards, and CRUD-heavy applications, the productivity gains were enormous.
The trouble is that the more productive a tool is, the easier it becomes to treat it as a worldview.
What starts as "you can write less SQL" quietly slides into "you don't need to know SQL at all." This happens with most programming rules: they begin as solutions to a specific problem, and over time the context falls away until they are recited like magic incantations. This is commonly called the Sorcerer's Apprentice dilemma.
ORM is no exception.
If you cannot read the SQL your ORM generates, the ORM stops being a convenience tool and becomes a black box. A black box looks like a gift when things are small, and looks like a coffin when something breaks.
To be fair, modern ORMs are far more mature than they once were, and the rules of thumb are well established: watch out for the N+1 Query Problem, do not blindly trust Lazy Loading, fetch the relationships you need explicitly, push complex queries into raw SQL or a query builder, and check the SQL your ORM produces in the logs. These lessons are now close to common knowledge, found in books and framework documentation alike.
So within a well-disciplined team and a small CRUD system, an ORM really does look like a gift box. Define your tables, declare your models, specify your relationships, and a surprising amount of work just happens on its own.
Of course, looking like a gift box does not eliminate the risks, but things do work well enough. The dangers are simply hidden behind conventions, configuration, logging rules, code-review guidelines, and framework defaults, ready to bite through the leash at any moment, but they work for now. Then one day a single list query fires as many SQL statements as there are customers, and the developer finally discovers there was a little query factory tucked inside the gift box all along. They take a deep breath and reflect on how much they have grown as a programmer, but in practice there is not much to worry about, because ORM abstraction leaks tend to collapse a system more slowly than the vendor who built it runs out of capital and shuts down.
8. Standardization: JPA and Entity Framework
As open-source ORMs like Hibernate gained widespread adoption, the Java world eventually moved toward standardization.
The overarching direction of Java EE 5 was "make development easier," and JPA (Java Persistence API) emerged as part of that movement.6
The significance of JPA was not simply to standardize on Hibernate alone, but to create a standard API for handling ORM in Java.
That said, this standardization was less a triumphant case of a standard leading the market and more a case of the standard catching up to reality.
EJB Entity Beans were the official Persistence model promoted by Java EE, yet developers had already been moving toward the lighter POJO-based Hibernate. JPA ultimately absorbed that momentum. It would be inaccurate, however, to view JPA as simply the standardization of Hibernate alone. The JPA Expert Group (JSR 220) drew on the designs of TopLink and JDO alongside Hibernate. Hibernate was certainly the decisive influence, but the shape of the standard was forged from the combined experience of multiple real-world implementations.
This kind of thing happens occasionally. While a committee was busy designing the mountain trail, people had already been taking the side path; later, the committee came along and put up a sign on that path. The sign reads "Official Trail."
Similarly, in the .NET world, Entity Framework emerged. According to Microsoft documentation, the initial release of Entity Framework shipped in August 2008 as part of .NET 3.5 SP1 and Visual Studio 2008 SP1, and since it predated Code First, it provided a Database First-centered O/RM based on EDMX and the designer.7
From this point on, ORM moved closer to being a core platform feature rather than a peripheral framework tool.
Java has JPA/Hibernate, .NET has Entity Framework, Python has Django ORM and SQLAlchemy, and Ruby has Rails Active Record.
ORM became a technology that is hard to avoid even if you dislike it.
9. Why ORM Keeps Getting Criticized
ORM raised productivity, but at the same time it bred a great many misconceptions. This is the Sorcerer's Apprentice dilemma mentioned above.
The first misconception is the belief that SQL disappears.
SQL does not disappear. It merely hides.
Code written with ORM is ultimately translated into SQL. The problem is that when developers do not know when that translated SQL executes, how many times, with what joins, or through which indexes, performance issues are discovered too late.
The second misconception is the belief that navigating an object graph is just as natural when it comes to database access.
foreach (var order in orders)
{
Console.WriteLine(order.Customer.Name);
}In object code, this looks perfectly ordinary. But with Lazy Loading in place, it becomes the N+1 Query Problem.
The third misconception is the belief that ORM replaces the relational model.
A database is not a simple storage container. It is a separate system with constraints, Transactions, indexes, Joins, aggregations, isolation levels, and execution plans.
Using ORM properly does not mean you can get away without knowing the database; it means you need to know the database. To a newcomer, this can sound like a scam: if the tool hides all of that, why do you need to know it? But abstraction was never meant to hide everything completely. It hides things in normal circumstances while still letting you go deeper when something goes wrong.

This is precisely why Ted Neward called ORM "The Vietnam of Computer Science."8 The phrase itself first appeared in a conference talk around 2004, and a write-up followed in 2006. It starts out easy to get into, but the deeper you go, the harder it becomes to get out. That is what makes ORM the "Vietnam of Computer Science."
Fowler strikes a similar balance when it comes to ORM animosity. His position is that the problems with ORM are genuinely hard, but going back to the 1990s-style pain where everyone had to hand-roll their own ORM is not the answer either.9
10. Micro-ORM and the SQL-first Backlash
As ORM tried to do more and more, tools pulling in the opposite direction gained momentum.
Micro-ORM tools typically take this kind of stance.
Write SQL directly.
Just reduce the repetition of mapping results to objects.Tools like Dapper belong to this camp, and they reject the ORM worldview entirely. They place little expectation on features like Unit of Work, Dirty Checking, or Lazy Loading. The developer writes SQL explicitly, and the tool simply handles mapping rows to objects.
SQLAlchemy in Python also occupies an interesting position. It is an ORM, yet it provides a strong SQL expression language (Core) alongside that. Looking at SQLAlchemy's release history, it has been around since 2006; version 1.4 introduced a unified Core/ORM query system, and version 2.0 established that direction as the primary way to use the library.10
Recent query builders, type-safe SQL tools, and projects like Prisma and Drizzle are, broadly speaking, grappling with the same concerns.
Developers still dislike being seen as incompetent for doing repetitive mapping work by hand, but they equally dislike ORM hiding SQL from them completely.
And so history has not settled neatly on one side.
Raw SQL:
Explicit, but repetitive.
Full ORM:
Convenient, but with hidden costs.
Micro-ORM / query builder:
Acknowledges SQL while trying to reduce repetition.11. NoSQL as a Detour
There was also a more radical approach to solving the ORM problem.
The idea was to stop mapping objects to relational tables and instead store documents that closely resemble objects directly.
Document databases like MongoDB promoted a JSON-like document model.11 If an application already handles data in JSON or object form, the thinking goes, why shred it into rows and joins? Just store it in a similar shape.
This was, in a sense, a detour around the ORM problem.
Traditional ORM:
Object → Table → Object
Document DB:
Document (similar to an object) → Document → Document (similar to an object)Of course, the detour was not free. Issues around joins, Normalization, Transaction management, duplicate data, and consistency came back in a different form. And as tends to happen with this kind of problem, some developers who tried to escape table hell ended up walking straight into document hell. Only the name of the hell had changed.
What is interesting here is that relational databases did not simply stand by and watch this trend unfold.
PostgreSQL strengthened support for types like JSON and JSONB, and the introduction of JSONB in PostgreSQL 9.4 was described as a feature that narrows the choice between relational and non-relational storage.12 In other words, the RDBMS absorbed some of the advantages of document databases.
So today's conclusion is not "relational is dead and NoSQL won."
Rather, what actually happened is this.
RDBMS:
Absorbing some aspects of transactions, Joins, constraints, SQL, and JSON
Document DB:
Flexible document model, rapid development, hierarchical data storage
Reality:
Both survive, each gradually borrowing strengths from the otherThe history of programming is like the history of theft. Good ideas eventually climb over the neighbor's fence.
12. Other Examples of Impedance Mismatch
ORM reduced some of the mismatch between objects and relational databases, but the friction that arises when connecting two different models has not disappeared. Today, a similar problem recurs between the backend domain model and the frontend UI state.
The backend typically produces domain objects, entities, DTOs, and API responses.
The frontend wants UI state.
These two are different things.
For example, on the backend, Order, Customer, Payment, and Shipment may each represent a natural boundary. But the UI wants "everything needed for the order detail page": the name, address, payment status, shipping status, button activation state, permissions, error messages, and loading state, all at once.
The same problems that arose between the database and objects now resurface between the API and UI state.
Past:
Object model != Relational table
Present:
Backend domain model != Frontend screen stateGraphQL is one answer to this problem. It tries to reduce the issues of REST endpoints giving too much (over-fetching) or too little, forcing multiple calls, by letting the client query only the fields it needs.13
tRPC aims to reduce type mismatches between server APIs and client calls in a TypeScript full-stack environment. Its official description leads with end-to-end typesafe APIs.14
History always repeats itself, and so does human folly.
ORM was a translator between the database and objects.
GraphQL, tRPC, BFF, and API composition layers are translators between the backend and UI state.
The difference is only location. In the past, the pain was greatest at the database boundary; today it has grown at the network/API/UI state boundary. Abstractions have advanced, but the friction that comes from bridging different models has to go somewhere. It does not disappear. It just moves.
13. How can ORM usage discipline be enforced within a team?
It is easy for an individual developer to decide "let's read the SQL that ORM generates." Making a team do the same months later is hard.
That is why ORM usage tends to split into one of two camps, "don't use it at all" or "use it for everything," but even so, here are some approaches people have collected for keeping ORM usage under reasonable control.
Enable SQL logging in development and staging, and review slow queries and query counts in pull requests.
Set an upper limit on the number of queries per API request. For example, if a single list query fires 200 SQL statements, the test should fail.
Attach N+1 Query Problem detection tools, such as Bullet for Rails, Hibernate statistics, or EF Core logging/interceptors.
Disable Lazy Loading by default, and enable it explicitly only where needed.
Add
Include,JOIN FETCH, and projection DTO query strategies to your code review checklist.Establish a rule that complex reports and aggregations go to SQL, views, or a query builder rather than an ORM entity graph.
In production, correlate slow query logs, execution plans, and DB metrics with application traces.
When defining a Repository or Query Facade, use names that reveal query intent rather than names that suggest an "ORM wrapper".
Good rules don't put developers in a position to fail. When people are busy, they forget about Lazy Loading, and on Friday afternoons nobody looks at execution plans. Explicit rules make it easy to stay on track.
14. Conclusion

All non-trivial abstractions, to some degree, are leaky
-Joel Spolsky, co-founder of Trello
The history of ORM is neither a story of "objects won" nor one of "relational databases won."
Both survived.
Object-oriented applications still model domain logic, services, and state transitions as objects.
Relational databases remain strong for transactions, constraints, joins, aggregations, and long-term data retention.
ORM is a tool for reducing the translation cost between the two.
Problems arise when ORM is treated not as a tool, but as a worldview.
A healthy approach to using ORM looks roughly like this.
Use ORM for simple CRUD operations and state changes.
Use SQL or a query builder for complex reads, reports, and aggregations.
Always know how to read the SQL that the ORM generates.
Treat Lazy Loading not as a convenient default, but as a dangerous feature.
Keep database constraints and Transactions as a safety net at a lower level than application code.
Don't assume the domain model and the database schema must be identical.
ORM is not a failed abstraction.
It is, however, a leaky abstraction.
And "leaky abstraction" does not mean you should throw it away. It means you should understand where it leaks before you use it. After all, every abstraction leaks to some degree.
A Brief Timeline
Period | Development |
|---|---|
1970 | Codd proposes the relational model |
1980s–1990s | Object-oriented languages and enterprise RDBMS each gain widespread adoption |
1990s | Early O/R mapping tools such as TopLink appear |
Early 2000s | Hibernate gains traction as a reaction to the heavyweight nature of EJB Entity Beans |
2002 | Fowler's PoEAA codifies the pattern language surrounding ORM |
2004 | Rails Active Record popularizes convention-based ORM |
2006 | JPA 1.0 and the initial release of SQLAlchemy |
2008 | Entity Framework 1.0 ships |
2010s | NoSQL/document databases, micro-ORMs, SQL-first approaches, and query builders gain widespread traction |
2014 | PostgreSQL 9.4 JSONB and similar RDBMS features absorb some of the advantages of document-style storage |
2015~ | The trend toward reducing data mismatches at API/UI state boundaries, including GraphQL, gains wider adoption |
2020s | Type-safe queries, schema-driven ORM, SQL regression, and end-to-end type-safe API flows coexist |
Footnotes
- E. F. Codd, "A Relational Model of Data for Large Shared Data Banks", Communications of the ACM, 1970. ↩
- IBM, "The relational database". ↩
- Oracle, "Introduction to TopLink". ↩
- Martin Fowler, "Catalog of Patterns of Enterprise Application Architecture". ↩
- Ruby on Rails Guides, "Active Record Basics". ↩
- Oracle, "The Java Persistence API - A Simpler Programming Model for Entity Persistence". ↩
- Microsoft Learn, "Past Releases of Entity Framework". ↩
- Ted Neward, "The Vietnam of Computer Science", 2006. ↩
- Martin Fowler, "Orm Hate", 2012. ↩
- SQLAlchemy, "Download / Release history" and "SQLAlchemy 2.0.0 Released". ↩
- MongoDB, "Document Database - NoSQL". ↩
- PostgreSQL Wiki, "What's new in PostgreSQL 9.4". ↩
- GraphQL, "GraphQL Queries". ↩
- tRPC, "End-to-end typesafe APIs made easy". ↩