Ontological Stability: The Hidden Principle Behind the Correct Use of Inheritance
Software developers often debate whether inheritance is "good" or "bad." The truth is more nuanced than a simple binary choice. Inheritance isn't always good, and it isn't always bad—it is highly effective in some domains and a complete failure in others.
The secret to knowing when to use it lies in a concept called Ontological Stability.
What is Ontological Stability?
Ontology is a fancy word for a simple concept: a list of things within a specific subject and the connections between them.
Think of it as creating a "map" of everything important in a topic and showing how those things relate. It is:
- A set of categories.
- The rules that describe how those categories are linked.
- Essentially, the "structure of knowledge" for that specific topic.
Ontological Stability refers to how much that map (the structure of knowledge) stays the same over time as your system grows.
Why Ontological Stability Matters for Inheritance
Inheritance is built on several key assumptions:
- The hierarchy structure is correct.
- The "is-a" relationship is true and remains true.
- A subclass will not behave in a way that contradicts its parent.
- The domain will not change so drastically that the hierarchy breaks.
If your domain's ontology is "stable," inheritance works beautifully. If it is "unstable" (meaning the definitions of things change frequently), inheritance becomes a nightmare of technical debt.
Modeling Examples: Stable vs. Unstable
Here are simple Object-Oriented examples (using Scala) to demonstrate stable vs. unstable inheritance through the lens of ontological stability.
1. Stable (Good)
In this example, the base class captures a constant "invariant" meaning.
sealed trait Vehicle {
def maxSpeedKmh: Int
}
final case class Car(maxSpeedKmh: Int) extends Vehicle
final case class Bike(maxSpeedKmh: Int) extends VehicleWhy it is stable:
- A
Vehicleis defined simply as "something that has a maximum speed." - You can add new types (Truck, Scooter, Plane) without changing the existing hierarchy or the definition of the parent.
- The "is-a" relationship (A Car is-a Vehicle) remains logically sound.
2. Unstable (Bad)
In this example, the base class is too broad, or its meaning is tied to attributes that change across different types.
sealed trait Vehicle {
def maxSpeedKmh: Int
def numWheels: Int // This is the problem
}
final case class Car(maxSpeedKmh: Int, numWheels: Int) extends Vehicle
final case class Bike(maxSpeedKmh: Int, numWheels: Int) extends VehicleWhy it is unstable:
- By adding
numWheelsto the baseVehicle, you have made a specific ontological claim: "All vehicles must have wheels." - The Breakdown: What happens when you need to add a
Sled(no wheels) or aBoat? - To accommodate these, you would have to change the parent class or give the
Boata nonsensical value likenumWheels = 0. - The ontology was not stable because the definition of "Vehicle" was too specific to certain sub-categories.
This is the translation of your deep dive into domains that benefit from Ontological Stability and how to model them correctly using inheritance.
Examples of Domains with Ontological Stability (Suitable for Inheritance)
A business domain is a prime candidate for Ontological Stability if its "Core Concepts" are slow to change and have a long lifespan. In these industries, changing a core definition can break hundreds of interconnected systems.
High-Stability Domains:
Banking & Finance: Concepts like Account, Transaction, Ledger, and Instrument are foundational and strictly defined.
- Insurance: Policy, Claim, Coverage, and Premium are regulated definitions that have remained consistent for decades.
- Healthcare: Patient, Encounter, Diagnosis, and Procedure must have stable meanings to ensure interoperability between different hospital systems.
- Supply Chain & Logistics: Shipment, Package, Route, and Warehouse form the physical and digital infrastructure.
- Legal & Government: Citizen, Permit, Case, and Statute require clear, legally-binding, and stable definitions.
Case Study: Banking Transactions
In banking, an Account is ontologically stable because its core definition—a "financial container with a balance"—is universally understood and regulated.
The Stable Base Model
Scala
sealed trait Account {
def accountId: String
def balance: BigDecimal
}
final case class Checking(accountId: String, balance: BigDecimal) extends Account
final case class Savings(accountId: String, balance: BigDecimal) extends Account
final case class LoanAccount(accountId: String, balance: BigDecimal) extends Account
Why this domain is stable:
- Regulated: Transaction types are governed by law.
- Immutable Meaning: An "Account" always implies a balance and an ID.
- Clear Relationships: The hierarchy is logically sound; no subtype contradicts the parent.
- Extensible: You can add new types (like
CreditCardAccount) without ever touching the baseAccounttrait.
The "Bad" Way: Polluting the Base Class
A common mistake occurs when developers try to force-fit attributes that don't apply to every subtype into the base class. Let's look at the "Interest Rate" trap:
// ❌ BAD PRACTICE: Putting specific attributes in the base trait
sealed trait Account {
def accountId: String
def balance: BigDecimal
def interestRate: BigDecimal // This assumes ALL accounts have interest
}
final case class Savings(accountId: String, balance: BigDecimal, interestRate: BigDecimal) extends Account
final case class Checking(accountId: String, balance: BigDecimal, interestRate: BigDecimal) extends Account // Awkward!
The Breakdown: Now, Account mandates that an interest rate exists for every account. This is factually untrue for many Checking or LoanAccount types. Developers often end up "hacking" this by setting interestRate = 0, which is a sign of poor modeling and low ontological stability.
The "Good" Way: Decoupled Capabilities
In a stable banking ontology, interest rates should exist at the Product or Capability level, not in the base Account class. Here are two superior approaches:
Option A: Modular Capabilities (Traits/Mixins)
Only apply the "Interest" capability to accounts that actually support it.
sealed trait Account {
def accountId: String
def balance: BigDecimal
}
trait InterestBearing {
def interestRate: BigDecimal
}
final case class Savings(accountId: String, balance: BigDecimal, interestRate: BigDecimal)
extends Account with InterestBearing
final case class Checking(accountId: String, balance: BigDecimal)
extends Account
Option B: Product-Based Modeling (Composition)
Link the account to a "Product" definition that holds the variable rules.
final case class Product(id: String, interestRate: BigDecimal)
final case class Savings(accountId: String, balance: BigDecimal, product: Product)
extends Account
Core Takeaway
Ontological Stability is the ultimate litmus test for inheritance.
Never put an attribute in a base class just because "most" children use it. Only put it there if it defines the very essence of the parent. If a concept like "Interest Rate" is variable, treat it as a modular capability or an external product rule. This keeps your hierarchy stable and your code resilient to change.
Summary
The decision to use inheritance should not be based on a desire to "save code" or "reuse functions." It should be based on the stability of the relationship.
- Use Inheritance when the relationship between the parent and child is an immutable truth within your domain (High Ontological Stability).
- Use Composition when the relationship is based on shared features or behaviors that might change or don't apply to every potential subclass (Low Ontological Stability).
If you build your hierarchy on a foundation that isn't ontologically stable, your code will eventually collapse under the weight of its own assumptions.