The Same Design Problem Hides Inside Every Table and Every Object

Kai Nguyen

Hatched by Kai Nguyen

Aug 07, 2026

11 min read

91%

0

What if the deepest similarity between a database table and a Python object is not that both store information, but that both are acts of refusal?

Each refuses to let every piece of data mean anything in every context. A table says: these values belong under these column names, and each column accepts only certain kinds of values. A class says: these attributes and behaviors belong together, and this kind of object can respond to certain operations. Both are ways of turning an undifferentiated mass into something a program can reason about.

That connection matters because software design is often presented as a choice between organizing data in databases and organizing behavior in objects. In practice, the harder problem comes earlier: how do we decide what deserves a name, a boundary, and a contract?

The answer determines whether a system remains understandable as it grows. It also reveals why many applications become fragile even when their code is technically correct. The trouble is rarely that the system lacks data or functions. The trouble is that its concepts have not been given stable shapes.

The Hidden Work of Giving Things a Shape

Consider a fruit stand. Before a database exists, there is only a messy reality: apples, prices, units, purchases, perhaps handwritten notes and changing inventory. A table gives this reality a structure:

CREATE TABLE fruit_stand (
  item TEXT,
  price NUMERIC,
  unit TEXT
);

The table does not merely store facts. It makes a claim about the world. It says that an item can be identified by a name, that a price is numeric, and that a unit is worth recording separately. The column names are not decoration. They are the vocabulary through which later queries will understand the data.

A Python class makes a similar claim:

class Fruit:
    category = "produce"

    def __init__(self, item, price, unit):
        self.item = item
        self.price = price
        self.unit = unit

    def label(self):
        return f"{self.item}: {self.price} per {self.unit}"

This class also turns a vague concept into a bounded one. A fruit has attributes that vary from instance to instance, such as its item and price. It may also have a shared property, such as its category. More importantly, it has a behavior, label, that expresses something the object knows how to do.

The database table and the class are not identical. One primarily organizes persistent records, while the other combines state with behavior. But they solve a related design problem: they make assumptions explicit.

Without a schema, the database is an unexamined pile of values. Without a class, related attributes and operations tend to scatter across a program. In both cases, the structure creates a surface that other parts of the system can rely on.

Good software does not merely preserve information. It preserves the meaning of information.

This is why naming conventions, data types, methods, and table definitions have consequences far beyond style. They determine what kinds of questions a system can ask and what kinds of mistakes it can detect.

Declarative and Imperative Thinking Are Complementary

SQL is largely declarative. When we write:

SELECT price, item FROM fruit_stand;

we specify the result we want, not the sequence of file operations required to obtain it. The database management system decides how to search, retrieve, and organize the result. We describe the destination and let the system choose much of the route.

Object oriented programming usually feels more imperative. We define a class, create an instance, call a method, and observe a change in state:

apple = Fruit("apple", 2, "kilogram")
print(apple.label())

Here, the programmer describes an interaction with a particular thing. The object carries data and exposes behavior. We are not simply asking for a set of results. We are telling a participant in the program to do something.

It is tempting to treat these as opposing philosophies. SQL asks what. Object oriented code appears to ask how. But the deeper distinction is not between two competing styles. It is between two levels of responsibility.

A database is good at answering questions over collections. An object is good at preserving the rules and operations associated with an individual concept. The database might answer, “Which fruits cost more than a certain amount?” An object might answer, “How should this fruit be displayed, priced, or validated?”

A robust application often needs both kinds of thinking:

  1. Collection level: What records satisfy these conditions?
  2. Entity level: What does one record mean, and what may happen to it?
  3. System level: Which changes are allowed, and which consequences follow?

Confusion arises when one layer is forced to do the work of another. If all domain behavior is reduced to scattered SQL statements, the rules become difficult to find and reuse. If all collection operations are forced through individual objects, queries can become slow, repetitive, or opaque.

The practical lesson is not to choose between tables and objects. It is to ask which questions belong to a collection and which belong to an entity.

A query such as “find all purchases made this month” belongs naturally to the database. A rule such as “a purchase cannot have a negative quantity” belongs naturally near the representation of a purchase. The boundary may vary by system, but the question remains stable: where should this rule live so that it is difficult to violate and easy to understand?

Structure Is a Contract, Not a Container

Beginners often encounter a table as a container and a class as a blueprint. Those descriptions are useful, but incomplete. A more powerful mental model is to see both as contracts.

A table contract includes names, types, and often implicit expectations. If a column is called price, users reasonably expect it to represent a monetary quantity, not a sentence or a date. If a column is called item, they expect its values to identify products consistently. The schema narrows interpretation.

A class contract includes attributes, methods, and the assumptions behind them. If an object exposes a label method, callers should not need to know how the label is assembled. If a child class overrides a method inherited from a parent, it is making a promise that the new behavior still fits the expectations established by the parent.

This is where inheritance becomes especially revealing. A child class can extend or override its parent, but that power has a hidden cost. The more a program relies on shared expectations, the more carefully those expectations must be defined. A subclass that retains the parent interface while quietly changing its meaning can create errors that are much harder to diagnose than a missing method.

Databases face a parallel problem. A table can begin with three simple columns, then accumulate exceptions: a text field that sometimes contains numbers, a unit field with inconsistent spellings, or a price that changes meaning depending on another column. The table still exists, and queries may still run, but the contract has eroded.

In both settings, structure creates leverage only when its promises remain credible.

A useful test is to ask what a new programmer can safely assume after reading the definition. If the answer is “almost nothing,” the abstraction is not protecting the system. It is merely hiding complexity behind a name.

This also explains the importance of conventions that seem trivial, such as consistent lowercase names, clear column names, semicolons, and readable formatting. Conventions reduce the cost of interpreting structure. They allow humans to recognize patterns quickly, just as data types allow software to reject certain invalid states.

Clarity is not cosmetic. It is a form of error control.

The Most Important Boundary Is Between Shared and Particular

One of the most useful ideas in class design is the difference between class attributes and instance attributes. A class attribute expresses what is shared by every instance. An instance attribute expresses what varies from one object to another.

That distinction is more profound than a Python detail. It is a general method for discovering the correct boundaries in a system.

Suppose every fruit in a shop is marked as belonging to the category “produce.” That is shared. The item name and price vary by instance. If we accidentally store a price as a class attribute, every fruit may appear to have the same price. The program has confused a property of the group with a property of the individual.

Databases face the same conceptual error. If a value varies by record, it belongs with the record. If it is shared across many records, it may deserve its own reference or table. Repeating shared facts everywhere creates inconsistency. Treating individual facts as global creates incorrectness.

This gives us a general design question:

Is this fact true of the kind, or is it true of this particular instance?

The question applies far beyond fruit stands. In a school system, the grading policy may be shared by a course while a grade belongs to one student. In a commerce system, a currency may be shared by a market while a price belongs to a product at a particular time. In a permissions system, a role may be shared by many users while an individual authorization may belong to one user and one resource.

Many software bugs are category errors of this kind. A value is placed at the wrong level of generality. Once that happens, every later operation must compensate for the mistake.

A practical design method is to classify every important fact into three categories:

  • Universal: true for every member of a type or system.
  • Instance specific: true for one particular entity.
  • Contextual: true only within a relationship, time period, or situation.

The third category is frequently neglected. A price may not belong permanently to a fruit at all. It may belong to a fruit, a shop, and a date. A simple table or class can hide this complexity until the system must support history, multiple locations, or changing policies.

Good modeling does not begin by asking where to put a field. It begins by asking under what conditions the fact is true.

From Named Structures to Living Systems

A class can be mutable by default. Its attributes may change after creation. A database is also mutable: statements can insert, update, and delete data. This flexibility is useful, but it creates a central danger. If every part of a system can change everything, no part of the system can reliably know what is true.

The answer is not to eliminate change. It is to make change legible.

A method can provide the only approved route for a state transition. A database statement can express a deliberate update. A constraint, validation rule, or clear naming convention can prevent an invalid state from entering unnoticed. The goal is to ensure that changes pass through places where their meaning can be checked.

Imagine a Fruit object with a public price attribute. Any caller can assign a negative number. The code may be simple, but the model is weak because the object cannot defend its own meaning. A more disciplined design could expose a method for changing the price and validate the value there.

Likewise, a table that accepts arbitrary values may seem convenient at first. Later, every query must remember to handle malformed data. The database has transferred its complexity to every future user.

This leads to a useful principle of system design:

Put rules at the narrowest boundary that can enforce them consistently, and expose the widest interface that users can understand safely.

The database should guard facts that must remain true no matter which application accesses the data. The object should guard behaviors and transitions that belong to the domain concept. The application should coordinate workflows that involve multiple entities or external systems.

These boundaries are not absolute, but they create a powerful division of labor. They prevent the same rule from being copied into dozens of places, where it will eventually drift.

Key Takeaways

  • Treat schemas and classes as contracts. When you name a column, define a type, or expose a method, state what other code may safely assume.
  • Separate collection questions from entity behavior. Use database queries to reason over many records, and objects to preserve the meaning and operations of individual concepts.
  • Distinguish shared, individual, and contextual facts. This prevents class attributes, table columns, and relationships from being placed at the wrong level.
  • Make valid changes easy and invalid changes difficult. Use types, validation, methods, and database rules to keep the system from accepting states that later code cannot interpret.
  • Design around meaning before mechanics. Ask when a fact is true and who must enforce it before deciding whether it belongs in a table, an object, or a service.

The first lesson of programming is often that computers manipulate data. The more important lesson is that software determines which distinctions count. A database table says that some values belong together. A class says that some data and behaviors form a recognizable thing. A query and a method then allow the rest of the system to interact with those decisions without reopening every underlying detail.

That is why the most consequential design work often happens before a single query runs or a single object is instantiated. It happens when a team decides that a value deserves a name, that a behavior belongs to a concept, or that a rule must be enforced at a particular boundary.

The mature programmer is not merely arranging data or writing functions. They are designing a world of stable meanings. The quality of that world determines whether later code feels like fluent reasoning or desperate repair.

Sources

← Back to Library

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 🐣