Dart 3 made real progress in how inheritance works and how classes connect with one another. The language now includes modifiers that define which classes can extend, implement, or combine with others. These modifiers (sealed, base, interface, final, and mixin) add structure to APIs, strengthen compile-time checks, and keep large projects safer and easier to manage. The update makes Dart more expressive while keeping behavior predictable across libraries.
How Dart’s Class Modifiers Work
Class modifiers in Dart in some ways reshape how developers structure inheritance. Rather than leaving intent to convention, these modifiers bring the compiler directly into that process. They define exactly what kinds of relationships a class allows and block anything outside those rules. That makes it easier to build libraries and applications where class hierarchies behave predictably. The language now gives authors the ability to describe intent directly in code, keeping APIs both expressive and safe from accidental misuse.
Why Class Modifiers Exist
Before Dart 3, inheritance was flexible but also fragile. A class written for one purpose could be extended or implemented in ways that broke assumptions about its behavior. If a library author wanted a base class to be reused through extension only, there was no way to enforce that at compile time. Developers had to rely on documentation and hope others followed it. Modifiers solve that problem by letting the compiler enforce those boundaries. The author of a class can now define whether others can extend it, implement it, or both. This reduces surprises when someone adds new code to an existing hierarchy. It also makes libraries more stable, since their public types behave the way they were intended.
A good way to see this difference is to compare older Dart behavior to the new modifiers:
That flexibility was useful but had a cost. Someone could implement PaymentProcessor without actually inheriting its shared logic, possibly breaking behavior that depended on its internal methods.
With modifiers, the class can define its inheritance intent directly.
This change tells Dart that PaymentProcessor should only be extended, not freely implemented. It’s a small shift in syntax that carries a big improvement in safety.
Sealed Classes
A sealed class creates a closed inheritance group where all subclasses must be declared within the same library. It’s used when the full set of possible types is meant to be known ahead of time. This is nice in code that relies on exhaustive type checks, such as state handling or pattern matching.
Every subclass of NetworkState must live in the same library. This guarantees that when a developer writes a switch expression, all valid cases are known at compile time.
Because the compiler knows the hierarchy is sealed, it can confirm that every branch of the switch covers all types. If one is missing, the developer gets a warning right away.
Sealed classes work well for modeling internal hierarchies too. Suppose an app defines a set of commands that should never be extended outside the core logic.
External packages can use these commands but cannot create new ones. That keeps the structure consistent and guards against unexpected behaviors added from other libraries.
Base Classes
A base class allows inheritance, but implementation from outside its defining library isn’t allowed. Any subtype that extends or implements it must be marked base, final, or sealed.
WordDocument extends Document and inherits its shared logic while still customizing parts of it. But no class outside this hierarchy can declare implements Document, because that would allow skipping its logic while claiming to follow its contract.
Another common example is a model where the base defines lifecycle hooks that subclasses must use properly.
Every subclass keeps access to the shared lifecycle logic, making sure to have predictable startup and shutdown behavior. If a developer tried to implement Service instead of extending it, the compiler would reject it. That small restriction helps maintain structural integrity when projects grow.
Interface Classes
An interface class defines a contract for others to implement, and it can include method bodies. Other libraries can implement it but can’t extend it. It’s meant for cases where behavior should be defined by agreement, not inheritance. This pattern keeps components loosely connected and easier to replace.
Other classes can implement this interface in any way they need.
This kind of interface is useful for building modular systems where different implementations follow the same rules but use distinct internal logic.
You can combine an interface with abstract to declare an abstract interface class. You can’t pair interface with base, final, or sealed because those occupy the same modifier slot.
That code combines the open contract of an interface with the controlled hierarchy of a base class. It gives the best of both worlds, flexibility to define multiple implementations and control over how inheritance behaves within one library.
The interface modifier encourages code that’s modular yet consistent. Each class promises certain behaviors without inheriting unwanted methods or dependencies. This keeps systems predictable while still giving space for creativity in implementation.
Mixins, Final Classes, and How They Interact
Modifiers in Dart 3 bring structure to how developers share and protect logic. Mixins and final classes help avoid tangled inheritance chains while keeping intent visible in the code. They give authors tools to share behavior safely, lock down APIs where needed, and keep class hierarchies easy to follow.
Mixins and Their Purpose
Mixins let developers reuse logic across classes without forcing inheritance. Instead of forming long class trees, mixins allow small, focused behaviors to be shared wherever they fit. This helps code stay flexible while keeping responsibilities separate.
The Logger mixin gives UserRepository logging ability without altering its inheritance path. It’s reusable across many classes that need similar functionality.
Mixins can also maintain state or interact with one another. That makes them ideal for features that appear across different areas of a project, such as caching or counters.
This class draws on both mixins without creating a deep hierarchy. The features stay self-contained, yet they work together naturally when combined in a single class. Mixins can’t be created as standalone objects, which keeps their purpose as shared traits intact.
Final Classes
A final class can be used freely, but outside its library it can’t be extended, implemented, or mixed in. That prevents unwanted overrides and makes it clear that its behavior is stable.
A final class like ApiClient can still be used anywhere, but its methods can’t be changed through subclassing. This prevents subtle errors that can appear when third-party code extends types in unexpected ways.
Final classes also work smoothly with mixins. That combination allows internal code reuse without giving away control over inheritance.
NetworkClient benefits from shared behavior without becoming open to external modification. The mixin stays reusable, and the final class guarantees that its core remains unchanged outside the defining library. This helps library authors expose reliable types without worrying about how others might alter them later. It also keeps complex APIs predictable while still giving room for internal flexibility.
Combining Modifiers
Class modifiers can interact to express deeper intent. A sealed class can include both base and final subclasses, while a base class can extend another base class or apply mixins for added features. Dart checks these relationships during compilation to make sure they stay consistent.
This structure defines all possible authentication states inside one library. External code can use them but can’t add new ones, keeping the state model predictable.
Mixins can also support these hierarchies by adding shared functionality without expanding inheritance chains.
This adds time tracking to every Session while still preserving control over inheritance. The mixin contributes behavior, and the base modifier keeps that structure safe.
These combinations give Dart’s inheritance model balance. Mixins provide reusable behavior, base and sealed classes define controlled extension, and final classes lock down behavior where it matters. Together they form a consistent system that promotes stability while keeping code expressive.
Why These Changes Matter
Older versions of Dart relied on convention to signal intent. A developer might write a comment warning that a class shouldn’t be extended, but there was nothing to stop someone from doing it anyway. That led to fragile systems where small changes could break dependent libraries.
Modifiers turn those expectations into enforceable rules. A mixin is now recognized as shared behavior rather than a class. A final class can’t be altered from outside its library. Base and sealed classes define specific relationships that the compiler protects. This model encourages consistency across projects and reduces maintenance headaches. Teams can share libraries with confidence, knowing their class structures won’t be changed in unexpected ways. It makes Dart’s object system clearer, more predictable, and easier to manage as projects grow.
Conclusion
Dart 3’s class modifiers bring structure to how inheritance behaves and how intent is expressed in code. Sealed, base, interface, mixin, and final each define boundaries that make relationships between types more predictable. They give developers control over how classes share logic or stay protected from unwanted changes. The language now treats inheritance as something deliberate rather than open-ended, creating a balance between flexibility and safety that helps large projects stay consistent as they evolve.

















