Last Updated: May 22, 2026
A nested class is a class declared inside another class. It's a tool for grouping a helper type with the one class that actually uses it, instead of dropping it loose in the namespace where any other code could grab it. This chapter covers the syntax, how access rules work between the outer and nested class, the difference between nested instance and static usage, and the kinds of design problems nested classes are good for (and the ones they're not).
Consider a Cart class that holds a list of items the customer wants to buy. Each item has a product name, a price, and a quantity. The item type isn't useful anywhere else in the program: nothing outside the cart needs to construct a "cart item" or reason about its shape. The standard place for that type is inside Cart itself.
LineItem is declared inside Cart. The rest of the program has no idea it exists. If another class tries to write new LineItem(...), the compiler says it can't see the type. Even spelling it as Cart.LineItem doesn't help, because LineItem is declared private inside Cart. The shape of a cart item is an internal concern of Cart, and that's exactly how the code reads.
The full name of a nested class is OuterClass.NestedClass. If you ever do reference it from outside (when it's public, which we'll get to), you spell it with a dot:
That OuterClass.NestedClass notation is also what shows up in compile errors and IntelliSense, so getting comfortable with it pays off.
A nested class is a full class in every other respect. It can have fields, properties, methods, constructors, and even its own nested classes. Inheritance works the same way (a nested class can inherit from another class, and it can be inherited by another class). The only thing that changes is where it lives and who can see it.
A class declared at the namespace level can only be public or internal. Those are the only two access modifiers allowed for top-level types, and the default is internal. Nested classes are different: they're members of their containing class, and members can use any of the six access modifiers. The default for a nested class is private.
The default of private is deliberate. Most nested classes exist because the outer class needed a small helper. Making them invisible by default keeps that helper from leaking into the wider code base.
When you want to expose a nested type as part of the outer class's public surface (a builder, a result type, an enumerator), you mark it public and callers use the dotted name:
Cart.Coupon is referenced from outside the class with the full dotted name. That name signals two things at once: "this type belongs to Cart" and "you only need this when you're working with carts."
A nested class can read and write the private members of its containing class. From the compiler's point of view, the nested class is "inside" the outer class, so the usual access checks treat its code as if it were part of the outer class itself.
This is one of the main reasons nested classes exist: a tightly-coupled helper that needs to poke at the outer class's private state can do so without forcing those fields to be made internal or public.
Receipt._order._items reaches into the private field _items on the Order instance and reads it. No internal accessor, no public property, no friend trick. The nested class has the same access the outer class does to its own privates.
That's a privilege, not a right. Reach in too eagerly and you create the same coupling problem you'd get by exposing the fields publicly: any change to the outer class's internal layout breaks every nested class that touched it. The good use case is when the nested class is part of the outer class's own implementation, not a general-purpose helper.
The reverse is also true: the outer class can access private members of the nested class. So if Receipt had a private field _headerLine, code inside Order could read it directly. The two are mutual.
In practice the outer-to-inner direction matters less, because the outer class is usually the one constructing nested instances and can just call constructors and public methods. The inner-to-outer direction is where the access-to-privates feature pulls its weight.
This is the most common confusion when people first see nested classes, especially if they've used inner classes in other languages: a nested class in C# does not have an implicit reference to an instance of the outer class. It's just a type that happens to be declared inside another type.
If a Receipt instance wants to access the data of a specific Order, the Order instance has to be passed in. That's what the Order order parameter in Receipt's constructor was doing in the earlier example.
Receipt is declared inside Order, but a Receipt instance is its own thing. It does not "belong to" a specific Order unless you wire that up yourself. The fix is to give Receipt an Order field and pass it in:
What the nested class does receive automatically is the access privilege we just covered: it can read private members of Order, but only when it has an Order instance to read them from.
You can also create a Receipt without any Order at all, if its design doesn't require one. There's nothing forcing every nested class to hold a reference to its outer type. Many nested helpers (a comparer, a builder, a tiny data record) are standalone and live entirely on their own.
The threshold belongs to the Cart namespace conceptually, but it's a plain little type with no link to any Cart instance. That's perfectly fine and very common.
A nested class doesn't change inheritance or polymorphism. It still has a Type of its own, it can implement interfaces, derive from base classes, and be used as a generic argument like any other type. The only thing that changes is its full name and its visibility.
The diagram below shows the containment relationship for a Cart that exposes a public Coupon and keeps a private LineItem.
Coupon is public, so outside code can construct it and pass it back to Cart. LineItem is private, so the world doesn't even know it exists. Both are full classes inside the type system; the only difference is the access modifier on the nested declaration.
The dotted-name rule also means you can have two unrelated types with the same simple name in different outer classes without any conflict:
Each Snapshot belongs to its outer class. The C# compiler treats Cart.Snapshot and Order.Snapshot as completely separate types. That kind of grouping is one of the small wins of nesting: you can keep type names short and let the outer class supply the context.
This is the part where C# differs most from languages with "inner classes." Two ideas are easy to mix up:
static, exactly like any top-level class. A static nested class has no instances and only holds static members.Let's start with a static nested class. It's just a grouping of static members under the outer type's name:
Cart.Defaults is a static nested class. You can't say new Cart.Defaults(), and trying to gets you CS0712 ("cannot create an instance of the static class"). All you do with it is reach in and use the static members.
A non-static nested class can still be used without a containing instance. The earlier Cart.Coupon and Cart.FreeShippingThreshold examples showed this: new Cart.Coupon("SAVE10", 0.10m) works fine even though no Cart was ever constructed. The "outer" class in C# is just a namespace-like scope; you don't need an instance of it to make nested instances.
That's a sharp difference from Java's non-static inner classes, which always carry a reference to an enclosing instance. C# never does that. If you want that behavior, you wire it up yourself by holding an outer reference in a field.
The decision flow for nesting:
The first question is reuse: if anything outside the outer class is going to need the type for its own sake, it doesn't belong as a nested helper. If it's tied to the outer class but callers do interact with it (a builder, a result type, an event-args type), make it public nested. If callers never touch it, keep it private nested. Only after you've chosen "nested" do you decide static vs not, and that comes down to whether the type has instance state.
Three patterns come up regularly in real code where a nested class fits.
Helper types that only the outer class uses. This is the Cart.LineItem case from the start of the chapter. The type exists because the outer class needs to store or pass around a small bundle of data, and exposing that type to the rest of the program would only invite misuse.
Entry is a private record-of-data type. Pulling it out into the namespace would expose new Entry(...) to every other class, even though the only sensible producer is Wishlist.Add. Keeping it nested and private locks down that contract.
Builder pattern types. When constructing a complex object requires many optional pieces, a builder collects them step by step and assembles the final object at the end. The builder is part of the object's public API, so it's a public nested class; building an instance any other way is usually not allowed.
Order.Builder is public so callers can use it, but it's nested because it has no life of its own outside Order. The dotted name Order.Builder reads as "the builder for an order." Order's constructor is private, so the only path to construct one is through the builder. That's a complete builder pattern. The full design discussion (mutability, validation, alternatives like records and with) belongs in the Design Patterns section.
Iterator and enumerator state types. When a class exposes a custom IEnumerable<T>, the underlying state machine that walks the data is usually a private nested class. Most of the time the compiler writes this for you when you use yield return, but the pattern matters when you implement the interface manually:
WishlistEnumerator exists only to walk a Wishlist. It needs access to the wishlist's items and has no other purpose. Private nested class fits: it's invisible to callers, but Wishlist.GetEnumerator() can return one because it's writing inside the same class. The manual pattern uses a nested class on purpose.
A nested class is an ordinary type. Constructing one allocates an instance on the heap like any other reference type. There's no extra overhead from being nested. If you're worried about per-element allocations (a hot iterator on a huge collection), look at struct enumerators; the nesting itself isn't the issue.
Nesting is easy to overuse. A type that started private grows, gets more callers (inside the same class), then someone wants to use it from another class and "just" makes it public. Two months later half the code base depends on OuterClass.NestedClass, and any change to OuterClass ripples through code that has nothing to do with it.
A few signs the type wants to be top-level instead:
OuterClass.NestedClass in callers more than new calls inside OuterClass. The type has escaped its container.internal so multiple classes in the same assembly can share it. At that point, it's not a helper of one outer class anymore; it's a normal internal type that belongs in its own file.C# 11 added a related feature called file-scoped types (the file modifier), which lets you declare a class that's visible only inside one source file. For some "helper that only this file uses" cases, that's a cleaner alternative to a public nested class scattered around a large file. We'll cover it properly in the Modern C# Features section; for now, the mental model is: nested classes group by "this outer class owns this helper", and file-scoped types group by "this one source file owns this helper".
OuterClass.NestedClass is the canonical way to refer to a nested type from outside. It also matches how the type shows up in reflection, in compile errors, in IntelliSense, and in the documentation generator. Three small habits keep this readable.
First, choose simple names that lean on the outer class for context. A Builder nested inside Order doesn't need to be called OrderBuilder. The dotted name Order.Builder already reads cleanly:
Second, when a using statement at the top of the file would help, use using static to pull in the outer type's members or using aliases for the nested type:
Catalog.Product.Review is the full nested path. The alias makes the call site readable. Use this sparingly; if you find yourself aliasing every nested type, the design has grown too deep.
Third, avoid nesting more than one level deep. OuterClass.MiddleClass.NestedClass is technically legal but rarely worth the cognitive cost. If a nested class is itself growing nested helpers, it's usually time to pull at least one level out.
A small program that pulls every concept from the chapter together: a Catalog class with a private Product nested class, a public Catalog.Review nested class for ratings, and a static Catalog.Defaults nested class for constants.
A few things worth pointing out about the design:
Product is private. Outside code has no way to construct a product directly or even know the type exists. The only path in is Catalog.AddProduct. The catalog owns its products end to end.Review is public. Callers do need to construct reviews and hand them to the catalog, so the type is part of Catalog's public surface. The validation (stars < 1 || stars > 5) lives in Review's constructor, which keeps Catalog from having to repeat it.Defaults is public static. It's a small bag of constants grouped under the catalog name. The grouping is the whole point: no plain FreeShippingOver floating around at the top level.Product has a List<Review> field. From inside Product the nested Review type is just Review. From outside both Catalog and Product, the same type is Catalog.Review. The name shifts based on scope, but the type is the same.new Catalog.Product(...). The class is private, so the compiler would refuse anyway, but more importantly, the design doesn't want it. Product is an implementation detail; Review and Defaults are not.That layered visibility is the practical payoff of nested classes. Each helper sits at the access level it actually needs, and the outer class's public surface is exactly what callers should see.