Code Rule #03: Be either declared final or abstract
- Mateusz Roguski
- Apr 19
- 7 min read
Updated: May 3

Table of Contents
Introduction
In modern software development, clarity is power. Every class we write communicates something — to future maintainers, to static analyzers, and increasingly to AI assistants. But what happens when we leave that intent ambiguous?
Every class should be either declared final or abstract. Never leave it undecided.
This rule is simple. But its impact is profound. It makes design explicit, protects your code from accidental extension, and reinforces a key architectural value: predictability.
Why “Open by Default” Is Dangerous
In many OOP languages, classes are open for inheritance by default. This sounds flexible — until it isn’t.
When a class is neither final nor abstract, it sends a mixed message:
Can it be extended?
Should it be extended?
Is it safe to override parts of it?
Or will extending it break everything silently?
The result is accidental extensibility — one of the most subtle and dangerous sources of technical debt. Someone subclasses your class just to override one method, and now the internal logic you never meant to expose is part of someone else’s fragile system.
The Rule in Practice
Use final when:
The class is meant to be used as-is.
You want to protect internal logic.
It’s a value object, service, handler, DTO, or command.
Use abstract when:
The class is meant to be extended.
It defines a partial implementation or contract.
You’re building a polymorphic base structure.
Language Landscape: Who Does It Well, Who Gets It Done, and Who Makes It Hard
This rule depends not just on discipline, but on language support. Let's break it down.
Languages That Get It Right by Design
These languages guide developers toward safe inheritance and clear intent by making final the default or encouraging composition-first design.
Language | final Support | abstract Support | Notes |
---|---|---|---|
Kotlin | ✅ Default is final; must opt-in with open | ✅ abstract keyword | Enforces immutability and explicit design |
Swift | ✅ Native final keyword | ⚠️ No abstract keyword; use protocols + stubs | Protocol-oriented design preferred over base classes |
Scala | ✅ final, sealed, private | ✅ abstract class, trait | Encourages sealed hierarchies and traits |
Dart (3+) | ✅ final class, sealed | ✅ abstract class | Powerful class modifiers, strongly typed |
These languages help teams write safer, more maintainable OOP code — not by accident, but by design.
Languages That Have All the Right Tools
These languages don’t enforce good design by default — but they provide clear, powerful tools to do so. The decision is left to the developer.
Language | final Keyword / Equivalent | abstract Support | Notes |
---|---|---|---|
Java | ✅ final | ✅ abstract | Open by default; relies on developer discipline |
C# | ✅ sealed | ✅ abstract | Same as Java; rich tooling support |
PHP | ✅ final | ✅ abstract | Open by default; modern PHP supports good discipline |
C++ | ✅ final (C++11+) | ✅ Pure virtual | Low-level control; abstract = pure virtual method(s) |
Objective-C | ⚠️ Compiler attribute | ⚠️ No native keyword | Final via attribute, abstract via pattern |
These languages give you the power — but not the guardrails. That’s where rules like this one shine.
Languages Where It Isn’t That Simple
These languages support object-oriented programming, but expressing final and abstract is either not enforced or only convention-based.
Language | final Equivalent | abstract Equivalent | Notes |
---|---|---|---|
Python | ⚠️ @final (not enforced) | ✅ Via ABC + @abstractmethod | Static hints only; no runtime enforcement |
Ruby | ⚠️ No keyword; use hook + raise | ⚠️ No keyword; simulate with stubs | Everything is open by default |
TypeScript | ⚠️ Pattern only (e.g., private constructor) | ✅ abstract class | Final via convention, not language |
JavaScript | ⚠️ Not supported natively | ⚠️ No native abstract class support | Can simulate with conventions or proxies; prototype chain is always open |
Important note
This comparison applies only to class-based object-oriented programming languages. Languages like Go, Rust, Haskell, or Elixir don't support classical inheritance and are therefore outside the scope of this rule.
A Note on Python: Popular, But Problematic
It’s worth calling out: Python is the most popular language in the world, and yet it has one of the weakest implementations of final and abstract.
Abstract classes require the abc module and custom metaclasses.
Finality is opt-in and not enforced at runtime — just a static suggestion (@final from typing).
You can still subclass, override, and mutate freely unless you build your own enforcement.
In a world where immutability, safety, and clarity are more important than ever — Python’s permissiveness isn’t just a quirk. It’s a liability.
This has serious implications for systems that value immutability, concurrent safety, and AI-assisted code generation. You can't rely on the language to protect your design — you must protect it yourself.
Why This Rule Matters
Makes intent explicit — either you're building a base class or a final block.
Protects internal logic — avoids subclassing hacks and unintended overrides.
Improves tooling — static analyzers, IDEs, and AI tools can reason better.
Promotes composition over inheritance — when inheritance isn't free, teams reach for safer, clearer alternatives.
Design Patterns: What This Rule Promotes and Prevents
Declaring every class as either final or abstract isn't just about keywords — it's about architectural discipline. And that means it reinforces some design patterns while discouraging others.
Patterns This Rule Encourages
Composition over Inheritance: This rule makes subclassing an explicit design decision. It encourages you to build systems out of collaborating objects instead of deep hierarchies.
Strategy: Abstract base classes define behavior contracts. You’re forced to model variation intentionally — for example, a PaymentStrategy interface with multiple concrete final implementations.
Template Method (only when abstract): If you use inheritance, it must be deliberate. The template method pattern fits well when you provide a base skeleton and force subclasses to fill in the blanks.
Decorator (if final): Decorating behavior using composition instead of inheritance works beautifully when your core objects are declared final.
Domain-Driven Design (DDD): Entities, Value Objects, and Services benefit greatly from this rule:
Value Objects are typically final,
Abstract Entities provide structure with clear extension points,
It makes the domain model stable, predictable, and immutable-friendly.
Patterns This Rule Discourages
God Object + Deep Inheritance Trees: If a class is neither final nor abstract, it’s often a grab-bag waiting to be extended in unexpected ways. This rule breaks that habit.
Inversion by Override (a.k.a. override-and-pray): Extending a class just to override one method becomes impossible unless the class was explicitly designed for that. That’s a good thing.
Inheritance-as-a-default: Many developers reach for inheritance out of habit. This rule forces you to stop and think: “Am I building a polymorphic family, or should I compose this behavior?”
Mixin Abuses (especially in Ruby or Python): Mixin-heavy designs often thrive on openness. This rule makes such behavior intentional rather than accidental.
In Short:
Promotes intentional, role-focused design.
Discourages lazy inheritance and subclass hacking.
It’s not anti-inheritance. It’s pro-decision.
Principles This Rule Promotes
Declaring every class as final or abstract pushes your code toward architecture that’s predictable, maintainable, and intention-revealing. It’s a small rule with a big ripple effect. Here are the core software principles it naturally reinforces:
Explicitness over Implicitness: This rule forces you to communicate your intent: is this class meant to be reused through inheritance, or used as-is? No more guessing. No more accidental subclassing.
Composition over Inheritance: By limiting inheritance to explicitly abstract classes, this rule nudges you toward building functionality via collaboration between small, focused objects — not fragile base class hierarchies.
Information Hiding / Encapsulation: Declaring a class final is a powerful way to preserve encapsulation. It protects your internal logic from being manipulated by a subclass that shouldn’t know your internals in the first place.
Robustness and Local Reasoning: When a class is final, you know it won’t be extended, so you can reason about its behavior in a self-contained way. That’s not true for open classes, where behavior might be overridden unpredictably.
Open/Closed Principle (OCP): This rule helps enforce OCP the right way: by making classes closed (final) unless they are explicitly abstract and designed for extension.
Principle of Least Privilege (applied to design surface): A non-final, non-abstract class invites subclassing by default — a violation of minimal exposure. Declaring intent makes your design surface smaller and safer.
Impact on AI-Driven Development
In AI-assisted software development, ambiguity is the enemy. When you leave a class open by default, you're not just sending a vague signal to your teammates — you're confusing the very tools meant to help you.
Language models don’t understand your intent — they infer it. And when your system doesn’t explicitly communicate which classes are meant to be extended or used as-is, these models are left guessing.
A class without final or abstract? It could be a base class. Or not.
A subclass that overrides a method that was never designed to be overridden? AI won’t warn you — it will assume you knew what you were doing.
Declaring a class as final or abstract removes the guesswork — and that makes your code a better interface for both human and machine collaborators.
When tools like code generators, refactoring assistants, and test suggesters can rely on clear inheritance boundaries, their output becomes dramatically better. They can:
Suggest safer extension points.
Detect misuse of base logic.
Generate tests and mocks that match real architectural roles.
And in reverse: open, ambiguous code is a hallucination trap. It invites AI to build on false assumptions. It looks flexible, but it's actually unstable.
If AI is part of the team, structure is part of the spec.
Summary — Be Either Declared final or abstract
"Be either declared final or abstract" isn’t just a stylistic rule — it’s about structure, safety, and clear intent in object-oriented design.
In most object-oriented languages, classes are open by default, which invites misuse, accidental inheritance, and fragile architecture. This rule closes that door — literally and figuratively.
By requiring explicit intent, you:
Prevent subclassing of classes never meant to be extended.
Protect internal logic and make refactoring safe.
Promote composition over inheritance.
Make your design clearer to both humans and machines.
Languages like Kotlin, Swift, and Scala enforce this discipline by design. Others like Java, PHP, and C# provide the tools — but leave the responsibility to you. And some, like Python and Ruby, make it difficult to express architectural intent at all.
This rule also aligns with foundational software principles:
Explicitness,
Encapsulation,
Open/Closed,
Least Privilege,
AI-readability.
When AI becomes part of the team, structure is no longer optional — it's essential.
So whether you're writing a service class, a domain model, or a reusable base, ask yourself the question:
Is this class meant to be extended?
No? Make it final.
Yes? Then it better be abstract.
Never leave it open by accident.