Last Updated: May 22, 2026
An abstract class is a class that exists to be inherited from, never to be instantiated on its own. It captures the shared shape of a family of related types, leaves the parts that vary unfinished, and forces every concrete subclass to fill those parts in. In a typical e-commerce codebase, abstract classes show up wherever you have a "category" of thing (a payment, a discount, a shipment, a notification) where every concrete variant shares some state and some behavior but does at least one important thing differently.
An abstract class is declared by putting the abstract modifier in front of class. That single keyword changes two things about the class:
new ThatClass(). The class becomes uncreatable on its own.Here is the smallest useful example. A Payment base type holds an amount, knows how to print a receipt header, and leaves the actual processing step unfinished:
This class does three things. It owns a property (Amount), it owns a finished method (PrintHeader), and it owns an unfinished method (Process). The unfinished one carries the abstract modifier and has no body, just a semicolon. Every concrete subclass has to provide a body for it. The base class draws the outline. The subclass fills in the details.
The terminology: a class marked abstract is called an abstract class. A class that is not marked abstract and can be instantiated directly is called a concrete class. A method (or property) without a body in an abstract class is called an abstract member. Concrete classes can have no abstract members because they must be instantiable. Abstract classes can have any mix of concrete and abstract members.
The reason for marking a class abstract usually falls into one of three buckets, and the same class often hits more than one of them.
Bucket 1: the class represents an incomplete design. Some classes describe a "kind of thing" that doesn't make sense on its own. A bare Discount with no rule for how to apply itself is meaningless. A bare Notification with no idea how to send anything is just data with no behavior. Marking the class abstract is how you say, in code, that this type is a template that needs to be completed before it can be used.
Bucket 2: there is shared state and behavior to factor out. Even if every concrete variant does at least one thing differently, they usually share most of their state. Every payment has an amount. Every discount has a label. Every shipment has a destination. The abstract base owns those fields and properties so subclasses don't have to duplicate them. That keeps the type family tight: one place to add a new shared field, one place to change a shared method.
Bucket 3: callers should work against the abstraction. Code that processes payments should be written against Payment, not against CardPayment or GiftCardPayment. Code that prices a cart should be written against CartItem. Marking the parent abstract is a way of telling readers (and the compiler) that this type is meant to be used through a base reference, with concrete subclasses providing the variation.
A concrete contrast helps. Compare a regular class with a virtual method and an abstract class with an abstract method:
The regular Discount lets you create an "empty" discount whose Apply returns the price unchanged. That works, but it's error-prone. Someone reading the code might forget to override Apply and end up with a no-op discount nobody intended. The abstract version closes that door. Forgetting to override Apply in a concrete subclass becomes a compile error instead of a silent bug.
The trade-off is straightforward. Regular classes are easier to start with (you can use them right away), and they're forgiving (a forgotten override is silent). Abstract classes are stricter (you have to subclass to use them) and more honest (they refuse to pretend an incomplete type is finished).
A new on an abstract class is a compile error, not a runtime error. The compiler catches it before the program ever runs.
The compiler reports CS0144 and refuses to produce a binary. There is no try/catch workaround at runtime; the code simply will not build. The same rule applies whether the abstract class has zero abstract members or many. Abstractness is a property of the class itself, not a side effect of having abstract methods.
The error message is precise, and the error code is worth recognizing. If you see CS0144 in a build log, it almost always means somebody wrote new X() for an abstract X.
There is one common follow-up question: what if I really need an "empty" instance, just to fill in a default? The honest answer is that abstract classes are saying you cannot. If you need a default, write a concrete subclass that supplies the missing behavior (often called the "null object" pattern), or stop using an abstract base for that type.
The empty variable is typed as Shipment (the abstract base), but the object it points at is a NoShipment (a concrete subclass). The next section unpacks that distinction, because it is the core of why abstract classes are useful at all.
An abstract class can never appear after a new, but it can appear almost everywhere else a type name is valid: as a variable's declared type, as a parameter type, as a return type, as the element type of a list, as a dictionary value type. The variable holds a reference to some derived, concrete object; the abstract base just describes what that object is guaranteed to support.
Three patterns are at work in that snippet, and they're the common shapes of how abstract classes get used:
Notification first = new EmailNotification { ... } declares a variable whose static type is Notification and whose runtime object is an EmailNotification. Method calls on first route through the abstract base's contract, but the actual code that runs comes from the derived class.List<Notification> is a collection of references to objects that satisfy the Notification contract, regardless of which concrete subclass each one is. You can mix kinds freely.SendAll(IEnumerable<Notification> items) is a method written entirely against the abstraction. It has no idea which kinds of notification exist. Add a third or fourth kind and this method does not change.That last point is the structural payoff. The abstract base is a contract; everyone speaks the contract; the variation lives only where it has to (inside each subclass). Compare that with a SendAll method written against a concrete type, or written against object with type checks: the abstract version is shorter, safer, and easier to extend.
A List<Notification> is a list of references, not of objects. Each element is a heap pointer to a separately allocated subclass instance. That's true of any reference type, but it shows up clearly here because the base type is abstract and you cannot avoid the heap allocation by trying to store the base directly.
The flip side of "abstract class cannot be instantiated" is "concrete subclass must be complete." If a class derives from an abstract base, it has to either implement every abstract member it inherited, or carry the abstractness forward by being marked abstract itself.
Here is what happens when a subclass forgets:
The compiler reports CS0534: "does not implement inherited abstract member 'Discount.Apply(decimal)'." The build fails. The fix is either to supply the override or to keep the subclass abstract:
Two things are happening in the second snippet. TenPercentOff overrides Apply directly and becomes a concrete class you can new. PercentDiscount overrides Apply (with a real body that consults the still-abstract Percent) but is itself abstract because it adds a new abstract member, Percent. Concrete subclasses of PercentDiscount then fill in Percent. The chain Discount -> PercentDiscount -> TwentyPercentOff shows how abstract classes can inherit from other abstract classes and pass unfinished work down the line.
The rule the compiler enforces is simple: at the moment a class becomes concrete (no abstract modifier on the class declaration), every abstract member it inherited, all the way up the chain, has to have a override somewhere on that path. The compiler walks the chain and double-checks.
Abstract classes have constructors, and those constructors run. They run from the subclass, called through base(...), the same way constructors run in any inheritance chain. The only difference is that nobody can call the constructor through new on the abstract class itself. The constructor exists to initialize the shared state every subclass will use.
(The timestamp on your machine will of course differ.)
The base constructor is marked protected, which is the conventional access modifier for an abstract class's constructor. public would technically also compile, but it would be misleading: nobody outside a subclass can ever call it (because the only legal caller is a subclass through base(...)), so protected documents intent. The compiler accepts either, but protected is the standard choice.
The order of execution: when new EmailNotification("alice@store.com", "Order shipped") runs, the runtime first calls Notification(string recipient) to set Recipient and CreatedAt, then runs the body of EmailNotification's constructor to set Body. Subclass constructors layer on top of the base; the abstract base goes first.
The same rules about constructor chaining (default constructors, parameterless requirements, base(...) calls) apply to abstract classes exactly as they would to any other base class. There's no special abstract-class twist there. For abstract classes, the only thing to remember is "the constructor is real, it runs from the subclass, and you can't call it via new."
Abstract classes are not stripped-down classes. They can have fields, properties (with or without backing fields), concrete methods, static members, events, indexers, constants, and operator overloads. The only restriction in the other direction is what we covered above: you can't new one. Everything else a normal class supports, an abstract class supports.
That single example shows a constant, a static field, a backing field, an auto-property, a property with validation, a protected constructor, a concrete instance method, an abstract instance method, and a static method, all on one abstract class. None of those are special to abstract classes; they're just the normal class building blocks. The lesson is that the abstract modifier doesn't subtract any features from a class. It only adds the rule "no direct new" and the ability to declare abstract members.
A useful way to think about it: the abstract base is the place to put everything that every subclass would otherwise have to copy. Shared state goes there. Shared helpers go there. Validation rules that apply to the whole family go there. Anything that varies stays abstract and lives in the subclass.
An abstract class can inherit from another abstract class. The child can pick up some of the parent's abstract members and implement them, leave others unimplemented, and add new abstract members of its own. The chain continues until some class is marked concrete, and that class has to have implementations for every abstract member up the entire chain.
Look at the chain. Shipment declares two abstract members. DomesticShipment and InternationalShipment each implement one of them (TaxRate) and leave the other abstract, so they have to stay abstract too. StandardDomestic and ExpressInternational implement the remaining abstract member (BaseCost) and become concrete. The compiler enforces this layer by layer: at each step, you either implement the missing piece or carry the abstractness forward.
This is a real design tool, not just trivia. It lets you express genuine "categories within categories." Every shipment has a destination. Every domestic shipment shares one tax rate; every international shipment shares another. Different concrete classes within each category compute their own base cost. You don't have to flatten the hierarchy or duplicate tax-rate logic in every concrete class. The intermediate abstract classes are where the partial commonality lives.
The diagram captures the layering. The top node is fully abstract: both members are unfinished. The middle nodes are partially abstract: TaxRate is done, BaseCost is still open. The bottom nodes are concrete: every abstract member is implemented somewhere on the path from the leaf up to Shipment. A reference typed as Shipment can hold any of the four concrete leaves, and a call to TaxRate() will dispatch correctly through whichever intermediate class supplied the implementation.
sealed does the opposite of abstract from one angle. A sealed class cannot be inherited from. An abstract class must be inherited from to be useful. Putting both modifiers on the same class is a contradiction, and the compiler rejects it.
The compiler reports CS0418: "'Discount': an abstract class cannot be sealed or static." There's no workaround because the two modifiers are mutually exclusive by design. An abstract sealed class would be a class you can neither instantiate (because it's abstract) nor subclass (because it's sealed), which is the empty set of usable classes.
For completeness: static and abstract also don't combine, and static and sealed are redundant (a static class is implicitly sealed). The combinations that are meaningful for a class declaration are roughly:
| Modifier combination | Meaning |
|---|---|
| (none) | Regular concrete class, can be instantiated and inherited from. |
abstract | Cannot be instantiated directly. Must be inherited from. Can have abstract members. |
sealed | Can be instantiated. Cannot be inherited from. |
static | Cannot be instantiated. Cannot be inherited from. All members must be static. |
abstract sealed | Compile error CS0418. |
abstract static | Compile error on the class. |
The abstract sealed case is a common confusion. People want a "this class is meant to be a base, but only this codebase can subclass it" effect, and sealed is not the way to get that. Use visibility modifiers on the constructor (a private protected constructor, for example) or internal access on the class itself.
A regular class with virtual methods can be subclassed and overridden, just like an abstract class. The differences are in what the compiler enforces and what the design communicates.
| Property | Regular class with virtual method | Abstract class with abstract method |
|---|---|---|
| Can be instantiated directly? | Yes | No |
| Can declare members with no body? | No (virtual methods need a body) | Yes (abstract members have no body) |
| Forces subclass to override? | No (override is optional) | Yes (concrete subclass must override) |
| Useful when base has a sensible default? | Yes | No |
| Useful when base has no meaningful default? | No (you'd have to fake a default) | Yes |
| Signals "this type is incomplete on its own"? | No | Yes |
The choice between them is almost always driven by one question: does the base have a meaningful default behavior for this method?
If yes (an Animal.Eat() that just prints "eating," a Cart.Validate() that returns true unless overridden), use a regular class with virtual. Subclasses override only when they have something different to say.
If no (a Payment.Process() that has no idea what charging means without a concrete payment method, a Notification.Send() that has no transport), use an abstract class with abstract. Forcing subclasses to override is appropriate because there is no honest default.
A simple test: try writing the default body for the method. If the only honest default is throw new NotImplementedException(), the method should be abstract and the class should be abstract. That throw is a runtime way of saying what abstract says at compile time, and the compile-time check is always better.
A short complete example pulls the pieces together. A CartItem abstract base captures everything a cart row needs, with most of the behavior concrete and one piece (the pricing rule) abstract.
The example exercises most of what this chapter covered. CartItem is an abstract class with a protected constructor that validates shared state. It has one abstract member, UnitPrice, that every subclass has to define. It has two concrete methods, LineTotal and PrintLine, that subclasses inherit unchanged. Subclasses pass arguments up through base(...). The cart loop is written entirely against the abstract base, and adding a fourth kind of cart item later would not require touching the loop.
If you remove the abstract modifier on the class and replace abstract decimal UnitPrice with virtual decimal UnitPrice => 0m, the code still compiles. But now someone could write new CartItem("Mystery", 1) and end up with an item whose price is zero, with no error. The abstract version refuses to let that happen. That is the payoff of marking a class abstract: errors that would have been runtime surprises become compile failures.
The diagram shows the same hierarchy at a glance. The * next to UnitPrice in the base marks it as abstract. The three subclasses each provide a concrete UnitPrice and inherit Name, Quantity, LineTotal, and PrintLine unchanged. The constructor is protected (the # symbol in UML), which matches the convention discussed earlier.