Object-Oriented Thinking for Entity Design

Practical considerations for entity modeling in business systems, and why inheritance is often avoided even by developers who are not opposed to it.

This note continues from Why Modern Developers Avoid Inheritance , Inheritance, Composition, and Meaningful Types , and Designing Meaningful Types .

Introduction

In the previous articles, I discussed why many modern developers tend to avoid inheritance, and why inheritance and composition need to be placed in the right architectural layer.

I do not personally reject inheritance as a concept. Cotomy itself uses inheritance in its framework foundation, and I still think inheritance is useful when the base type has stable meaning.

At the same time, when I look back at the business systems I have actually built, I notice something very simple. In entity design, inheritance almost never survives.

This article is about that specific point.

Scope of This Article

This is not an attempt to explain object-oriented theory from the beginning.

There are many ways to interpret object-oriented design, and I do not claim that my own practice should be treated as a universal rule.

What I want to describe here is narrower than that. This is a summary of how I have tended to model entities in real system development, and why inheritance usually did not survive for long when I tried to introduce it there.

So the focus is practical rather than theoretical.

How Modeling Usually Starts in Real Projects

When I start a new system, the first step is usually not class design.

Normally I begin by identifying the business domain, talking with the people involved, and organizing the required operations. I suspect many engineers would recognize roughly the same flow in their own work. In a reasonably formal project, that often leads to use cases and use case descriptions before the model is fixed.

In smaller internal systems, however, the process is sometimes lighter than that.

If I am building a small system for internal use and I already understand the business domain well, I may not write every use case in a formal document. I may postpone class diagrams as well. In some cases, I implement entity classes directly while the model is still being clarified through the screen and operation flow.

More recently, AI tools have made it much easier to reconstruct documentation from an implemented system. Because of that, there are times when I knowingly take a shortcut: implement the system first, and organize the use cases or model documentation afterward.

I do not think that is the right approach for larger systems. But when the whole system is small enough to build in one or two months, the cost of a design mistake is limited, and iteration can be faster than trying to freeze the entire model too early.

The important point is not whether the process looks formal. The important point is whether the model becomes clear enough before it spreads through the system.

Why Entity Modeling Matters So Much

Once the business domain and the goal of the system are understood, the next step is to design the model.

That part matters more than almost anything else.

What the system represents, how that information is persisted, how screens read it, and how operations change it are all built on top of the model. If the model is unstable, every screen and every API becomes harder to keep consistent.

For that reason, entity design is one of the areas I treat most carefully.

Why Inheritance Rarely Survives There

Cotomy uses inheritance in the framework itself. CotomyElement and CotomyForm are obvious examples, and that structure is intentional because those classes represent stable roles in the UI layer.

Entity design usually does not feel like that.

I may consider inheritance briefly when I first see similar fields across several entities. But in real business systems, those similarities are often shallow. Once the operational meaning of each entity becomes clearer, the common base class usually starts to feel forced.

I have created such inheritance structures before, but they were often removed later.

A Typical Business Example

A simple example is an internal order management system with three entities: Estimate, Order, and Shipment.

At first glance, they look similar. Each one has line items, and each line item may contain a product code, quantity, and amount.

That superficial similarity can make inheritance look attractive. It is easy to imagine a shared transaction base with a shared line type underneath it.

The Inheritance Model That Looks Reasonable at First

This is the kind of structure that an experienced engineer would probably reject very quickly, but that can still look quite reasonable when design experience is limited. I remember it feeling more persuasive to me earlier on than it does now.

classDiagram

class Transaction {
  id
  date
}

class TransactionLine {
  productCode
  quantity
  amount
}

class Estimate
class Order
class Shipment
class EstimateLine
class OrderLine
class ShipmentLine

Transaction <|-- Estimate
Transaction <|-- Order
Transaction <|-- Shipment
TransactionLine <|-- EstimateLine
TransactionLine <|-- OrderLine
TransactionLine <|-- ShipmentLine
Estimate "1" *-- "*" EstimateLine
Order "1" *-- "*" OrderLine
Shipment "1" *-- "*" ShipmentLine

At first glance, this feels reasonable. The names are familiar, the shared fields look obvious, and the duplication appears to disappear neatly.

If the design discussion stops at field similarity, this kind of diagram can easily look like a good object-oriented model.

Why That Model Usually Breaks Down

The first problem is naming.

I have a hard time finding a base concept that remains both meaningful and stable across Estimate, Order, and Shipment. Calling them all Transaction may look convenient, but the meaning is already unstable. An estimate is not the same kind of thing as an order in operational terms, and a shipment is not simply another version of the same concept.

This is where the meaningful type question becomes important again. Even if several entities share some fields, that does not mean there is a meaningful base entity waiting to be discovered.

The Domain Differences Matter More Than the Shared Fields

The real problem becomes clearer once the business differences are examined.

An estimate may have the same product appear more than once for different price options. An order usually expects each product to appear only once. A shipment goes further: its lines represent physical dispatch units, not copies of order lines, and what gets shipped in what grouping is driven by logistics operations rather than the order structure itself.

These are not variations on the same concept. Each entity carries different rules, different operational identity, and a potentially different structural shape from the start.

The line items look similar. They do not mean the same thing.

These are not minor variations around one stable concept. They represent different stages of business operation with different rules, different identities, and different future change directions.

Looking at the three entities separately makes this structural divergence easier to see.

classDiagram

class Estimate {
  estimateNo
  date
}

class EstimateLine {
  productCode
  quantity
  unitPrice
}

class Order {
  orderNo
  date
}

class OrderLine {
  lineNo
  productCode
  quantity
  amount
}

class Shipment {
  shipmentNo
  orderNo
  shipDate
}

class ShipmentLine {
  orderLineNo
  shippedQuantity
  trackingNumber
}

Estimate "1" *-- "*" EstimateLine
Order "1" *-- "*" OrderLine
Shipment "1" *-- "*" ShipmentLine

The differences are visible once each entity stands on its own. EstimateLine does not require a unique row key at the product level, because the same product may appear more than once for different quantity tiers. OrderLine uses a line number to uniquely identify each row, since the same product is not expected to appear twice. ShipmentLine links back to the original order line and carries a tracking number, which neither estimate nor order lines need.

That is why the inheritance line starts to become dangerous. The shared structure suggests a stronger conceptual unity than the domain actually provides.

Why the Shared Base Becomes a Liability

One might ask whether a shared base class used only as a holder for line items could have worked. If the line structures were truly identical across all three entities, that argument might have some weight. But that condition is rare in practice. And even when the structures happen to look identical at the start of a project, the moment an inheritance relationship is committed to, it introduces a structural constraint that is difficult to undo once screens, services, and persistence rules have formed around it.

Later, when the estimate logic grows in one direction and the shipment logic grows in another, the base class becomes a place where old assumptions remain embedded in the structure. The coupling is not only about shared code. It is about shared shape.

Inheritance is strong structural coupling. It fixes both structure and meaning at the same time. Once several screens, services, and persistence rules depend on that hierarchy, separating the entities again becomes expensive.

If the domain is likely to diverge, that strength becomes a cost rather than a benefit.

What Actually Gets Reused

That does not mean reuse disappears.

In practice, reuse often exists somewhere else. For example, the UI parts used to display or edit line items may be shared. In the order management system I described, I do share a CotomyElement subclass that handles the visual structure of a line item row. That shared part works across estimate, order, and shipment screens without any of those entities needing to carry an inheritance relationship to each other.

That kind of reuse feels much safer because the shared concern is local and visible. It does not pretend that the entities themselves all mean the same thing.

The Structure I Prefer Instead

When I want to share behavior across entities, I usually prefer interfaces, composition, or just independent classes that happen to follow the same rule.

Interfaces are especially useful when the shared concern is behavioral rather than structural. Unlike inheritance, they do not require the classes themselves to share one structural line. That makes them easier to apply across different entity types when only a narrow cross-cutting operation needs to be shared.

In practice, I usually define an interface when some part of the system needs to treat several entity types under the same narrow rule. In other words, the interface is often driven by a real cross-cutting requirement rather than by a desire to make the model look more abstract.

A simplified version of that idea looks like this.

classDiagram

class ILineItemContainer {
  lineItems
}

class ILineItem {
  productCode
  quantity
}

class Estimate {
  lineItems
}
class EstimateLine {
  productCode
  quantity
  unitPrice
}

class Order {
  lineItems
}
class OrderLine {
  lineNo
  productCode
  quantity
  amount
}

class Shipment {
  lineItems
}
class ShipmentLine {
  orderLineNo
  productCode
  shippedQuantity
  trackingNumber
}

Estimate ..|> ILineItemContainer
Order ..|> ILineItemContainer
Shipment ..|> ILineItemContainer
EstimateLine ..|> ILineItem
OrderLine ..|> ILineItem
ShipmentLine ..|> ILineItem
Estimate "1" *-- "*" EstimateLine
Order "1" *-- "*" OrderLine
Shipment "1" *-- "*" ShipmentLine

This kind of structure lets shared logic work at two levels. Container-level logic can depend on a line collection contract, while line-level logic can depend on a smaller shared interface for the fields that really are common.

That is one of the main advantages of interfaces here. They let me define only the common operation or contract that needs to be shared, without pretending that the full class structure is also common.

At the same time, the concrete line types remain separate. That is important because the lines are not interchangeable even if some operations can treat them through the same narrow contract.

That freedom matters more to me than removing a little duplication from the class definitions.

Conclusion

Avoiding inheritance in entity models is not the same thing as rejecting object-oriented programming.

The real issue is that business entities often represent different meanings even when they look superficially similar. When that is true, forcing them into a shared base class usually creates more trouble than it removes.

That is why, in my own work, inheritance appears much more naturally in framework foundations than in entity design. The framework roles stay stable. Business entities often do not.

For the same reason, Cotomy does not assume entity inheritance in its application patterns.

Design Series

This article is part of the Cotomy Design Series.

Series articles: CotomyElement Boundary , Page Lifecycle Coordination , Form AJAX Standardization , Inheritance and Composition in Business Application Design , API Exception Mapping and Validation Strategy , Why Modern Developers Avoid Inheritance , Inheritance, Composition, and Meaningful Types , Designing Meaningful Types , and Object-Oriented Thinking for Entity Design.

Previous article: Designing Meaningful Types

Next article: Entity Identity and Surrogate Key Design

Learn Cotomy

Cotomy is a DOM-first UI runtime for long-lived business applications.