This note continues from Why Modern Developers Avoid Inheritance and Inheritance, Composition, and Meaningful Types .
Introduction
In the previous article, I introduced the idea of meaningful types as a practical way to think about inheritance and composition.
That idea is simple to state, but it still leaves an important question behind. How do developers actually recognize a meaningful type while designing a system?
I do not think that judgment appears immediately when someone first learns object-oriented programming. In many cases, it develops slowly through experience, after seeing both useful inheritance and misleading inheritance in real work.
Why Inheritance Often Feels Unnatural at First
When I first learned object-oriented programming, inheritance did not feel impossible to understand, but it did feel difficult to use naturally.
I could follow the syntax. I could understand what a base class was supposed to be. But when I tried to design real structures for myself, it was not always obvious where inheritance genuinely belonged.
That difference matters. Understanding the language feature is not the same thing as having design intuition for it.
It takes time before the structural role of inheritance starts to become visible. Until then, inheritance can feel like something that exists in textbooks and framework code, but not something that naturally appears in everyday design decisions.
I sometimes suspect that this is one reason inheritance is so often avoided in modern frontend work. Many developers may simply not have had enough chances to build intuition for it.
There is probably another reason as well. Modern composition-oriented design is often strong enough to let teams build quite large features without falling into obvious structural collapse. If composition already provides a practical way to scale a screen, a feature, or a local UI structure safely, then the pressure to learn inheritance deeply is naturally weaker than it may once have been.
There may also be a broader shift in how frontend developers think about structure in the first place. In many modern frontend environments, especially those shaped strongly by React and Vue, developers may not think in classes very often at all. React now teaches function components and Hooks as the modern default, and Vue 3 recommends Composition API rather than class-based components. That does not mean classes disappeared everywhere. Angular, for example, still uses TypeScript classes directly. Even so, across a large part of the frontend world, class-centered design is no longer the main mental model.
If most design experience happens in that kind of environment, the situations where inheritance feels structurally natural may remain hard to see. It is not only that inheritance is difficult. It is also that many developers can continue building useful systems without being forced to develop much intuition for either inheritance or class-based design itself.
State Modeling That Looks Like Type Modeling
Part of the confusion comes from the kinds of modeling examples developers encounter early on.
I remember seeing designs where different runtime states of an object were expressed as subclasses, even though the object itself remained the same kind of thing.
An estimate, for example, might be represented with subclasses such as UnderReviewEstimate and CompletedEstimate.
classDiagram
class Estimate
class UnderReviewEstimate
class CompletedEstimate
Estimate <|-- UnderReviewEstimate
Estimate <|-- CompletedEstimate
At first glance, that can look object-oriented. It appears to classify objects into more specific forms. But in most business systems, that structure is not actually describing a type hierarchy. It is describing state.
If the estimate is still an estimate before review, during review, and after completion, then those differences are usually better expressed as instance state, transition rules, and behavior tied to that state. They are not necessarily separate types.
I saw similar patterns in other places as well. Sometimes customers were divided into MaleCustomer and FemaleCustomer as though sex itself defined a different structural type. In other cases, staff members were split into several subclasses only because they belonged to different internal classifications at the moment, even though they were all still staff members inside the same operational model.
I should add an important limitation here. Much of what I saw came from Japanese contract development environments, including projects where I was sent into client sites in an SES-style arrangement. Because of that, I cannot claim with confidence that these examples reflect some global standard pattern. It is entirely possible that my experience is somewhat skewed, or that developers in other environments would find these examples much less familiar. I vaguely remember seeing similar modeling in books as well, but I am not confident enough in that memory to treat it as evidence. If this does not match your own experience, it is probably best to take this part as one engineer’s partial observation rather than as a universal rule.
Of course, those distinctions can matter in business processing. They may affect permissions, workflow, display rules, reporting, or legal handling. But that still does not automatically make them good candidates for inheritance. In many cases, they are closer to attributes, classifications, or state carried by an instance than to a durable type hierarchy.
That is what made inheritance difficult to understand early on. The code looked object-oriented, but much of it was not expressing stable type meaning. It was expressing runtime state, mutable classification, or business-side labeling through subclasses.
Once that kind of modeling becomes common, inheritance starts to feel vague. It no longer looks like a way to represent structural roles. It starts to look like a slightly formal way to draw branches around whatever distinctions happen to exist in the current business rules.
Seeing that kind of modeling early makes inheritance harder to understand, because the structure looks like object-oriented design while quietly representing something else. What is being drawn as a type hierarchy is often only a runtime status flow or a temporary classification boundary. Once that distinction becomes clear, many earlier inheritance structures start to look misplaced.
Why Frameworks Show Inheritance More Clearly
Inheritance tends to appear more naturally in framework foundations than in everyday application modeling.
In application domains, many relationships are contextual. A screen contains forms. A process uses a service. An entity moves through states. Those relationships are often operational rather than structural.
Framework infrastructure is different. There, stable roles are easier to identify.
Form is a structural role. A concrete screen form can inherit from that role.
A base service abstraction is a structural role. A more specific service implementation can inherit from that role.
A controller boundary is a structural role. Individual controllers can extend it while preserving the same architectural responsibility.
These are not incidental similarities. They are stable positions in the architecture.
An External Service Example
External service boundaries are a practical example of the same idea.
An application may first define a broader ExternalService role for integrations that live outside the application boundary. Under that, it may define more specific service roles such as StorageService, EmailService, or KeyValueStoreService. The application should then talk to those stable application-side abstractions rather than to each external API directly.
Concrete implementations can be built under each role. A storage branch might include AzureBlobStorageService, S3StorageService, and, in some older environments, FtpStorageService. An email branch might later grow into SendGridEmailService or SmtpEmailService. A key-value branch might eventually separate into RedisKeyValueStoreService or another provider-specific implementation. Those systems all behave differently, expose different APIs, and carry different operational assumptions, but from the application’s point of view they can still belong under stable service roles.
classDiagram
class ExternalService
class StorageService
class EmailService
class KeyValueStoreService
class AzureBlobStorageService
class S3StorageService
class FtpStorageService
ExternalService <|-- StorageService
ExternalService <|-- EmailService
ExternalService <|-- KeyValueStoreService
StorageService <|-- AzureBlobStorageService
StorageService <|-- S3StorageService
StorageService <|-- FtpStorageService
In that structure, inheritance is not being used to collect convenience methods. It is being used to express meaningful roles in the system at more than one level.
The application-level types remain stable, while each provider-specific implementation encapsulates the differences in API shape, authentication style, error handling, and operational details.
That is useful for several reasons. It isolates service-specific behavior behind consistent application boundaries. It stabilizes the architecture seen by the rest of the application. It lowers the learning cost for other developers working in the codebase, because they can depend on clear service roles instead of learning each provider separately. It also reduces the impact of future changes in external provider APIs.
This kind of structure feels natural because each base type already means something on its own. Even before seeing any concrete subclass, ExternalService, StorageService, EmailService, and KeyValueStoreService can all read as meaningful architectural roles. Each of them describes a stable architectural responsibility rather than merely a place where shared methods happen to live.
Meaning Comes Before Reuse
That is the point I now find most useful.
Inheritance works best when the base class represents a meaningful role before any subclasses are added. In other words, the base type should still make sense as a type even if no subclass existed yet.
Framework boundaries often satisfy that condition. UI structural types often satisfy it. Service abstractions often satisfy it.
What usually causes trouble is the opposite pattern: a base class is introduced only because several classes happen to share code.
My own impression is that this difference becomes easier to see if I compare it with older structured-programming habits. In that style of development, many engineers tried very hard to avoid writing the same processing twice. That was understandable. If a function could be defined clearly through its input and output, then extracting common logic was often relatively straightforward. Of course that approach could still break down as systems grew, but the act of classification itself was usually simpler.
Object-oriented design feels different to me. There, the most important question is not whether two pieces of code look the same at first glance. The more important question is whether the class name is appropriate and whether the type still has independent meaning as a type.
Shared code can justify refactoring, but it does not automatically justify inheritance. Reuse alone does not prove that a meaningful type exists.
Even when the same code appears in multiple places, I do not think it is safe to conclude immediately that those places are expressing the same thing. Sometimes the logic is truly shared. Sometimes it only happens to look similar because different parts of the system currently pass through a similar step. That distinction is easy to lose if reuse becomes the first design goal.
Once I started looking at inheritance that way, many design choices became easier to evaluate. The question stopped being whether two classes looked similar enough. The more important question became whether the proposed base type still made structural sense as a type.
Recognizing Meaningful Types Takes Time
This also explains why the previous article could not end with a simple formula.
It is easy to say that inheritance should be used only where the abstraction remains meaningful. It is harder to recognize that meaning consistently without experience.
Developers usually build that intuition gradually. They see frameworks where inheritance fits naturally. They also see application code where inheritance was used to represent convenience, temporary similarity, or object state, and where the structure became harder to reason about as a result.
Over time, the distinction becomes clearer. The problem is not inheritance itself. The problem is whether the design is expressing a stable structural role or only forcing reuse into a hierarchy.
Conclusion
Inheritance often becomes confusing when it is used to represent convenience or temporary similarity.
It feels much more natural when the base type expresses a stable role in the architecture.
That is why meaningful types matter so much. They provide the most reliable way I know to judge when inheritance belongs in a design and when composition is the more honest structure.
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 , and Designing Meaningful Types.
Previous article: Inheritance, Composition, and Meaningful Types
Next article: Object-Oriented Thinking for Entity Design