The Hidden Boundary Between an Object and a Row
Hatched by Kai Nguyen
Sep 01, 2026
11 min read
3 views
94%
What If Your Program and Your Database Disagree About Reality?
A Python object and a database row can describe the same customer, product, or purchase. Yet they do not mean the same thing. One is a living participant in a process. The other is a durable record that can be selected, compared, modified, and combined with countless other records.
This difference explains one of the most persistent problems in software design: the application feels like a world of objects, while the database behaves like a world of tables. Developers often treat the two as interchangeable representations. They are not. They are two different answers to two different questions.
An object asks: what can this thing do? A row asks: what do we know about this thing?
The deeper challenge is not learning Python classes or SQL statements separately. It is learning how to move responsibly between behavior and evidence, between a changing process and a shared record of that process. Once that boundary becomes visible, many design choices become easier.
A class organizes possibility. A table organizes memory.
That distinction is more useful than the simplistic idea that a table is merely an object written down.
Two Kinds of Structure: Blueprint and Record
A class is a blueprint. It describes the attributes and methods that instances should have, but it does not itself contain the data of any particular instance. When a Fruit class defines an item name, a price, and a method for displaying the fruit, it describes a type of participant in the program.
An instance is different. It is a concrete object with actual values. One instance might represent an apple priced at 2. Another might represent an orange priced at 3. The class defines the shape and capabilities of these objects, while each instance carries its own state.
A database table has a remarkably similar first layer. Its columns define names and data types. Its rows contain the actual values. A table called fruit_stand might have columns named item, price, and unit, while each row records one particular offering.
This resemblance is useful, but it can also mislead us. A class and a table both provide structure, yet they structure different kinds of knowledge:
- A class says what an object is able to do.
- A table says what facts have been stored about many objects or events.
- An instance is a participant in a running program.
- A row is a data point in a persistent collection.
Imagine a shopping cart. In Python, a ShoppingCart object might contain items and provide methods such as add_item(), remove_item(), and total_price(). The object is not merely a container. It embodies rules. It can decide whether an item may be added, calculate a total, or change its own state.
A database table called cart_items might contain a cart identifier, a product identifier, a quantity, and a price. It stores the ingredients from which a cart can be reconstructed. But the table does not automatically know what it means for a cart to be valid. It does not inherently possess the cart's behavior.
This is the first important connection: a class is a behavioral contract, while a table is an informational contract. Both impose order, but one organizes actions and the other organizes claims.
The Tension Between Local Behavior and Shared Truth
Object oriented programming encourages us to bundle properties and behavior together. This bundling makes software more manageable because the rules for changing an object's state can live beside the state itself. A bank account can expose a withdraw() method rather than allowing every part of the program to manipulate its balance directly.
That arrangement creates a useful form of locality. The object can protect its internal assumptions. If the account must never become negative, the withdrawal behavior can enforce that rule.
A database has a different priority. Its data is shared. Many program processes, users, and services may need to search or modify it. SQL therefore emphasizes high level requests such as selecting certain columns from a table or inserting a new row. The requester describes what data it wants, while the database management system handles the lower level work of locating and organizing that data.
This is the characteristic power of declarative language: you state the desired result without spelling out every operational step. In Python, you may write a loop that visits objects one at a time. In SQL, you can ask for every item whose price exceeds a threshold, leaving the database to determine how to retrieve the matching rows.
The two styles reflect two different relationships to state:
- Python objects usually express a sequence of decisions. What should happen next?
- SQL queries usually express a condition of interest. Which stored facts satisfy this request?
Neither style is superior in every situation. A program needs imperative behavior to carry out decisions, validate inputs, and coordinate actions. A database needs declarative access to efficiently search a large shared body of information.
The trouble begins when one side is forced to impersonate the other. If application code retrieves every row and manually filters it, it treats the database like a passive filing cabinet. If database queries are expected to contain every piece of business behavior, the application becomes difficult to reason about. The system loses the clarity that comes from assigning each concern to the environment best equipped to handle it.
Consider a fruit stand with ten million sales records. An application could retrieve all of them, create an object for each row, and then calculate which products sold most often. That approach turns a database question into an object processing problem. A query can express the selection and aggregation closer to the data, returning only the result the application actually needs.
Now consider a rule such as, “A shopping cart cannot contain more than twenty items unless the customer has a wholesale account.” That is not merely a retrieval question. It is a behavioral rule involving context and a decision. It belongs naturally in application behavior, even if the final cart state must be stored in the database.
The practical principle is simple: ask the database for patterns in shared facts, and ask objects to govern meaningful behavior.
Mutability Is Where the Boundary Becomes Dangerous
Custom Python objects are mutable by default. Their attributes can change after an instance is created. This is convenient because real processes change. A cart gains an item, an employee changes departments, and a reservation moves from pending to confirmed.
But mutable objects create a difficult question: who has the authority to change the state?
Suppose a Product object has a price attribute. One part of the program changes it to 5. Another part changes it to 4.50. A third part has already copied the old value into a cart. The object model permits change, but it does not by itself guarantee that every representation of the product changes consistently.
A database introduces a second layer of mutability. The row can also be modified, possibly by another process or user. Now the product has at least two representations, and both can change. The central problem is no longer “how do I map this object to this row?” It is “which representation is authoritative, and under what conditions may it change?”
This suggests a useful design framework called the state ownership triangle. Every important piece of state should have clear answers to three questions:
- Authority: Which system is allowed to decide the correct value?
- Behavior: Which component is allowed to perform a meaningful transition?
- History: Which component preserves the durable record of what happened?
For example, an application object might own the behavior of calculating a discount. The database might own the durable record of the resulting price. A sales ledger might preserve the historical transaction so that later price changes do not rewrite the past.
Without these distinctions, a team may update an object and assume the database reflects it, or update a row and assume an existing object has noticed. The code may appear correct in a single process while failing as soon as multiple processes share the same data.
A safe workflow makes transitions explicit. Instead of exposing arbitrary changes to a cart's internal list, provide a method such as add_item(). That method can enforce a rule, calculate a new total, and then persist the resulting state. The database should receive a deliberate update rather than a scatter of unrelated attribute assignments.
The same principle applies to reading. A database row should not automatically be treated as a perfectly current object. It is a snapshot obtained at a particular moment. Creating an object from that row means bringing past information into a running process. The object may then become stale.
A mutable object is a decision in motion. A database row is a decision that has been recorded.
Good software does not eliminate the difference. It makes the transition between the two visible.
Why Inheritance Does Not Translate Cleanly Into Tables
Inheritance provides another revealing contrast. In Python, a child class can take on attributes and methods from a parent class, then override or extend them. This is a mechanism for sharing behavior across related types.
Imagine a parent class called Employee, with a method for calculating a basic allowance. A child class called Manager might inherit that behavior and add a management bonus. The relationship says more than “these records share columns.” It says “these objects participate in a common behavioral family, with specialized rules.”
A relational table, by contrast, has columns and rows. It does not naturally contain executable methods or behavioral inheritance. You can represent employees and managers with separate tables, a single table with a type column, or a combination of related tables. Each choice stores a different arrangement of facts, but none reproduces the full meaning of a class hierarchy.
This leads to a broader insight: similarity of data does not prove similarity of behavior. Two rows may have the same columns while being governed by different rules. Conversely, two objects may share important behavior while their data is stored in several tables.
This is why automatic object to table mapping can be seductive. It promises a one to one translation: class becomes table, instance becomes row, attribute becomes column, method becomes nothing. The first three mappings may work well for simple cases, but the missing method is not a minor omission. It is the most important part of the object.
A useful architecture therefore treats the database as a source of durable facts and the object model as a source of operational meaning. The object can assemble information from multiple tables, apply rules, and expose a coherent interface. The tables can remain optimized for querying, sharing, and persistence rather than being forced to imitate the shape of the program's classes.
This does not mean the two schemas should be unrelated. Names, data types, and constraints should communicate with one another. Consistent naming, explicit column types, and carefully chosen boundaries reduce translation errors. But correspondence should be treated as a negotiated interface, not as a law of nature.
A Practical Method for Designing the Boundary
When deciding whether a fact belongs in an object, a table, or both, use four tests.
1. Is it a durable fact or a temporary decision?
A customer's name and an order's total may need durable storage. A cached display string or a temporary validation flag may exist only during a program's execution. Do not burden the database with every transient detail, and do not trust an in memory object to preserve information that the business must retain.
2. Does changing it require a rule?
If a value can be changed safely by direct assignment, it may be ordinary state. If changing it requires validation, authorization, calculation, or coordination, represent the change as behavior. A method such as approve_order() communicates more than setting status to a new string. It marks a meaningful transition.
3. Will many actors need to search it?
Facts that must be filtered, sorted, compared, and combined across users belong naturally in relational storage. SQL is designed for high level requests over collections. Let the database answer collection questions instead of reconstructing the entire collection as objects.
4. What must remain true even if the application fails?
Some rules belong in application methods. Others must be protected by the database because multiple applications may write the same data. If duplicate email addresses are forbidden, relying only on a Python check can create a race between two simultaneous requests. Durable integrity deserves durable enforcement.
These tests produce a healthier division of labor. Objects manage meaning at the boundary of action. Tables preserve facts at the boundary of time. The database query retrieves the smallest useful set of information, and the object model gives that information behavior where behavior is genuinely needed.
Key Takeaways
- Do not equate a row with an object. A row is stored evidence. An object is state plus behavior inside a running process.
- Assign ownership before writing code. For every important field, decide who has authority, who performs transitions, and who preserves history.
- Use SQL for collection questions. Filtering, sorting, and aggregation over shared data are usually better expressed as declarative queries than as application loops.
- Make meaningful changes into methods. A method such as
add_item()orapprove_order()can enforce rules that a freely mutable attribute cannot communicate. - Treat mapping as translation. Similar names and columns help, but a database schema and an object model serve different purposes and need not be identical.
The most reliable systems are not those that erase the boundary between code and data. They are the systems that make the boundary legible. They know when a value is a fact, when it is a decision, and when a decision has become history.
We often say that software models the real world. That phrase hides an important choice. Are we modeling the things that exist, the actions those things can take, or the records left behind by those actions?
A class answers the second question. A table answers the third. Confusing either for the first is how systems become brittle. The world is not made of objects alone, nor of rows alone. It is made of changing entities, constrained actions, and traces that outlast the moment.
The art of software design begins when we stop asking how to make an object look like a row, and start asking what kind of truth each representation is responsible for.
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 🐣