The Hidden Argument Between Objects and Rows
Hatched by Kai Nguyen
Aug 22, 2026
11 min read
1 views
92%
Why does a program represent a customer as a living object while a database represents that same customer as a row of values? The difference looks like a technical detail. It is not. It is a disagreement about what reality is made of.
In one world, a customer has identity, behavior, changing state, and relationships to other objects. In the other, a customer is a record that can be selected, grouped, joined, updated, and counted. One world asks, “What can this thing do?” The other asks, “Which facts satisfy these conditions?”
Most difficult software systems live between these two worlds. Their reliability depends less on mastering either objects or SQL in isolation than on understanding the boundary between them. The central challenge is not translating syntax. It is deciding which representation should own which kind of truth.
Good software does not eliminate the difference between objects and rows. It gives each representation a clear responsibility.
Two Ways of Knowing the Same Thing
Object oriented programming begins with the idea that properties and behaviors belong together. A class acts as a blueprint, while an instance contains real data. A BankAccount class might define a balance and a method called withdraw(). Each account instance has its own balance, but all accounts share the same general rules for withdrawing money.
This structure is powerful because it allows a program to treat a thing as more than a bag of fields. The account can protect its own invariants. It can reject a withdrawal that exceeds the balance. It can record a transaction. It can change its representation without forcing every caller to understand its internal details.
A relational database starts from a different intuition. It separates data into tables and exposes a language for asking questions about that data. A customer table might contain an identifier, a name, and a credit limit. An order table might contain an identifier, a customer identifier, and a total. SQL then makes it possible to retrieve individual records, combine tables, group results, calculate aggregates, and update stored facts.
The database is not primarily concerned with what a customer can do. It is concerned with what can be stated about customers as a collection. It excels at questions such as:
SELECT customer_id, SUM(total)
FROM orders
GROUP BY customer_id;
That query does not ask an order to calculate its own contribution. It asks the entire collection to reveal a pattern.
This leads to a useful distinction:
- Objects are good at local meaning. They hold state alongside the operations that interpret or change it.
- Relations are good at global meaning. They make it easy to compare, filter, combine, and summarize many facts at once.
The same business concept may therefore need two legitimate representations. A customer object is useful when an application is deciding what to do next. A customer row is useful when an organization is asking what has happened across thousands or millions of customers.
The mistake is assuming that one representation should replace the other.
The Boundary Is Where Meaning Gets Lost
Suppose an application loads an account from a database and creates a Python object from it. At first, the mapping seems straightforward. The account identifier becomes an instance attribute. The balance becomes another instance attribute. The account class supplies methods such as deposit() and withdraw().
But the conversion quietly raises difficult questions.
Where does the account's identity live? Is it the database identifier, the memory address of the Python object, or both? If the object changes its balance, when does that change become a durable fact? If two objects represent the same database row, which one is current? If a method changes one object, what prevents another process from overwriting that change with stale data?
These are not merely implementation questions. They are questions about authority.
A mutable object suggests that state can change directly. A database introduces persistence, concurrency, transactions, and shared access. In memory, changing account.balance may appear immediate and private. In a database, changing the corresponding row may affect every user of the system and must often be coordinated with other changes.
This is why naive object to row conversion creates fragile systems. Developers may treat a row as if it were an object, then discover that it has no behavior. Or they may treat an object as if it were a row, exposing every attribute to unrestricted updates. In both cases, the boundary becomes a leak.
Consider a transfer between two accounts. A simplistic design might do this:
source.balance -= amount
target.balance += amount
That expresses the local state changes clearly. But a real transfer also requires validation, a transaction, consistency if the program fails halfway through, and a durable record of what occurred. The object can express the rule that a balance must not become negative. The database can ensure that the debit, credit, and transaction record become visible together.
Neither layer is sufficient alone. The object understands the operation. The database protects the operation's consequences.
The most important question at a system boundary is not “How do I map this field?” It is “Which layer is responsible for preserving this invariant?”
An invariant is a condition that must remain true. An account balance cannot be negative. An order cannot be shipped before payment is authorized. A username must be unique. Some invariants are local and behavioral. Others concern many records and require the database's collective view.
A practical design begins by assigning each invariant to its strongest guardian:
- Put rules about one object and its immediate state near the object.
- Put rules about uniqueness, relationships, and collective consistency in the database when possible.
- Put workflow rules that coordinate several actions in an application service or transaction boundary.
This division prevents a common failure mode: duplicating the same rule in many places and assuming the copies will remain identical forever.
Inheritance Does Not Mean Database Hierarchy
Object oriented design also introduces inheritance. A child class can take on attributes and methods from a parent class, then override or extend them. This is a model of behavioral specialization. A PremiumAccount might inherit from Account while changing withdrawal limits or adding rewards.
Relational databases do not naturally represent this kind of relationship. A table does not inherit methods from another table. It stores facts, and SQL combines those facts through keys, joins, views, and queries.
This distinction matters because programmers often try to force a class hierarchy directly into a table hierarchy. The result may be a parent table for common fields, child tables for specialized fields, and elaborate queries to reconstruct an object. Sometimes that design is appropriate. Often it creates a tax that is paid on every read.
The deeper issue is that inheritance describes substitution of behavior, while a foreign key describes a relationship between facts. These are not equivalent forms of structure.
If every premium account can be used wherever an ordinary account is expected, inheritance may express a meaningful behavioral contract. If a customer has many addresses, however, a relationship between customer and address is probably clearer than a class hierarchy. One concept concerns what an object can do. The other concerns how many facts are associated with another fact.
A useful test is to ask whether the relationship changes behavior or merely records association.
If it changes behavior, a polymorphic object model may be useful. If it records association, relational modeling is usually the more direct language. Confusing the two creates systems that are elegant in diagrams but awkward in queries, migrations, and maintenance.
This also clarifies the role of class attributes. A class attribute can represent a value shared by every instance of a class, such as a default policy or a fixed category. But shared application behavior is not the same as shared database data. A policy stored as a class attribute is part of the program's definition. A policy stored in a table is data that can be changed, audited, queried, and governed.
The choice depends on whether the value is a rule of the software or a fact of the business.
That is a powerful architectural distinction. Code defines capabilities; data records commitments. When a business value must be inspected, changed by authorized users, reported on, or preserved historically, it usually belongs in the data model rather than being hidden in a class definition.
Queries Are Not Failed Methods
There is a temptation to believe that every operation should be attached to an object. If objects bundle data and behavior, perhaps a collection of objects should answer every question through methods. But this would discard one of the database's greatest strengths: reasoning about a set without individually visiting each member.
Imagine a list of one million order objects. To find monthly revenue by region, the application might load every order, inspect each one, group them in memory, and calculate totals. A relational query can perform the grouping where the data already lives, often using indexes and execution strategies designed for this purpose.
The difference is not only speed. It is conceptual clarity. A query expresses a question over a population:
SELECT region, COUNT(*)
FROM orders
GROUP BY region;
An object method usually expresses a decision about one particular entity:
order.can_be_cancelled()
These operations belong to different scales of thought. One is about membership and distribution. The other is about meaning and action.
A strong application respects this difference. It does not turn every query into a loop over objects, and it does not force business behavior into anonymous SQL fragments scattered throughout the code. Instead, it gives each form of reasoning a home.
For example, a reporting screen may need total revenue, average order value, and the number of inactive customers. These are relational questions. A checkout process may need to determine whether a particular order can be cancelled. That is an object or domain question. A service that cancels the order and records the event is a coordination question.
The three questions may involve the same underlying data, but they should not be collapsed into one abstraction.
This suggests a simple mental model: objects are verbs attached to nouns, while queries are questions attached to collections. The first helps a system act safely. The second helps a system see clearly.
Designing the Translation Instead of Hiding It
The boundary between an object model and a relational model should be treated as an explicit translation layer. That does not necessarily mean a large framework or a complicated architecture. It means acknowledging that translation has costs and making those costs visible.
A useful translation process has four stages.
1. Identify the business identity
Determine what makes two representations refer to the same thing. A database key may be the durable identity, while the Python instance is only a temporary representation. Make this distinction explicit so that equality, caching, and updates do not rely on accidental memory identity.
2. Separate state from behavior
Not every method belongs in the persistence model, and not every column deserves to become a public attribute. Decide which state must be stored and which behavior should protect or interpret that state.
For example, an order may store status, but callers should not freely assign any string to it. A method such as mark_shipped() can enforce the allowed transition. The database can add a constraint or transaction rule to prevent impossible values from being persisted.
3. Define the unit of consistency
Ask which changes must succeed or fail together. A single attribute update may need no elaborate coordination. A transfer, subscription renewal, or inventory reservation usually spans several records and demands a clear transaction boundary.
The object model can make the operation readable. The relational system can make its outcome atomic.
4. Design for the questions you need to ask
Data modeling is not only about storing objects. It is about making important questions affordable and trustworthy. If the business needs to know which products sell together, the schema and queries should support that question directly. Do not bury all useful information inside serialized object state merely because it is convenient to reconstruct an object later.
This is where aggregation and grouping become architectural tools, not just SQL features. They reveal that a database is not merely a basement for application objects. It is an analytical instrument with its own form of intelligence.
Key Takeaways
- Assign authority deliberately. Let objects guard local behavior and let databases guard durable, shared, and collective facts.
- Do not confuse relationships with inheritance. Use inheritance for behavioral substitution, and relational links for associations among records.
- Keep set questions set based. Filtering, joining, grouping, and aggregation generally belong close to the database rather than inside loops over objects.
- Make identity and consistency explicit. Decide how an in memory object corresponds to a durable record and which operations must be atomic.
- Treat translation as design work. Mapping fields is easy. Preserving meaning across mutable objects and persistent rows is the real engineering problem.
The Real Architecture Is a Division of Perspective
Objects and rows are often presented as competing models, as if one must eventually defeat the other. That framing produces endless arguments about which paradigm is more natural. A better view is that they answer different questions about the same system.
An object asks: What is this thing allowed to do, and how can it preserve its own integrity? A relational database asks: What facts exist, how are they connected, and what patterns emerge across the whole collection?
The mature system does not force either perspective to imitate the other. It lets objects be precise about action and lets relations be powerful about evidence. It builds a boundary where identity, invariants, transactions, and translation are named rather than assumed.
The surprising lesson is that the object relational divide is not mainly a clash between programming styles. It is a clash between acting in the world and knowing about the world. Software becomes dependable when it recognizes that those are different activities, then gives each one the representation it deserves.
The best architecture is therefore not the one that makes objects look like rows or rows look like objects. It is the one that knows when a thing should behave like an individual and when it should be understood as part of a population.
Sources
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 🐣