Back to Fundamentals: Mastering Class, Record, Interface, Internal, and Sealed in C#
Stop guessing which C# keywords to use. Master the CIDIS model to choose between class, record, interface, internal, and sealed with confidence. Learn how focusing on these C# fundamentals reduces bugs, improves code quality, and helps your team release faster.

If you are unsure when to use a class, record, or interface in C#, mastering these fundamentals is essential for reducing bugs and writing cleaner code. By applying the CIDIS model (Class for identity, Record for data, Interface for contracts, Internal for scope, and Sealed to prevent inheritance), you can make deliberate design choices that enhance software maintainability and scalability.
Refer to the following checklist to determine which CIDIS element is appropriate for your scenario:
-
Class: Does this type need a unique identity and changing state?
-
Record: Is this type mainly for passing around immutable data?
-
Interface: Do you need to define a contract for behaviour that multiple types can implement?
-
Internal: Should this type or member be hidden from other projects within the solution?
-
Sealed: Is this class complete and should not be extended further by inheritance?
Uncertainty about when to use a class, record, or interface in C# or whether to apply the internal or sealed modifiers can impede development, reduce code quality, and complicate collaboration. Many developers encounter these challenges, especially during sprints or when unclear code structure delays releases.
Drawing a parallel from sports, where experts advise teams to "get back to basics" during challenging times, focusing on C# fundamentals is equally crucial for developers. Mastery of these basics accelerates release cycles and reduces bugs. For example, in a recent C# project, my team encountered issues with excessive inheritance. By revisiting core language features, we simplified a complex module. True progress often results from mastering these foundational elements.
What is the difference between a class and a record in C#?
A class is a reference type for objects with unique identity and changing state, while a record is a reference type for immutable data that uses value-based equality. Use a class for business logic and entities. Use a record for data transfer objects (DTOs) or configurations.
At first, these seem similar. Both can have properties and methods. However, they serve different purposes. A class represents identity. By default, comparing two class instances checks if they refer to the same object in memory. If two User objects have the same values but different memory locations, they are not equal. You should use a class for domain entities like users or orders.
Records implement value-based equality, meaning that if all properties match, the objects are considered equal. They also support non-destructive mutation, allowing the creation of new objects from existing ones using the with keyword. These characteristics make records well-suited for API requests or events in messaging systems. Selecting the appropriate type reinforces clean coding practices.
Of course, there are exceptions. Sometimes a record may make sense for a domain entity, especially if immutability and value-based equality are desired, and your architecture supports it. Likewise, there are cases where using a class for a DTO is justified, such as when you need to support mutations, track state changes over time, or add custom methods or behaviour to your data objects. Understanding these nuances helps you choose the right tool for your specific project needs.
Using Interfaces as Design Contracts
An interface defines what a type can do, not what it is. It specifies the capabilities a type must provide. If you only need to define a contract without implementation, reach for an interface. This is a powerful tool for professional, enterprise-grade applications.
Interfaces are essential for promoting loose coupling, as they enable substituting implementations without modifying the calling code. This approach facilitates testing by allowing the use of mocks or stubs in place of actual databases. For instance, implementing an IClock interface enables testing of time-dependent logic with a specified test time. Mastery of interfaces is fundamental to effective .NET development.
When should you use the internal access modifier?
Use the internal modifier to limit the visibility of a type or member to the current assembly or project. This hides implementation details from external projects. It ensures a clean public API and prevents other teams from depending on your internal logic.
In C#, the internal modifier restricts code accessibility to the same compiled unit, such as a DLL or EXE file. For example, marking a repository that manages database details as internal prevents external projects from accessing it directly. Test projects can access these types by using the InternalsVisibleTo attribute. However, exposing internals for testing can inadvertently expand the API surface, introduce dependencies, or complicate future refactoring. Maintaining internals visible to tests also incurs ongoing maintenance costs, so this practice should be regularly reviewed as the codebase evolves. Such encapsulation is essential for preserving code integrity.
Why should you seal your C# classes?
You should use the sealed modifier when a class is complete, and you want to prevent other developers from inheriting from it. Sealing a class protects its behaviour from being altered. It also provides small performance benefits by allowing the runtime to skip certain checks when calling methods.
When a class is sealed, the Just-In-Time (JIT) compiler can invoke methods directly rather than consulting a virtual table, resulting in marginally faster method calls. More significantly, sealing a class communicates that its design is final and prevents modification of critical logic, such as security token generators. The sealed modifier should be used to enforce design decisions and maintain loose coupling in C#. The CIDIS Model: A Shortcut for Better Code
To keep these choices simple, I use a mental model called CIDIS. It stands for:
- Class is for Identity.
- Interface is for Contract.
- Data is for the record.
- Internal is for the scope.
- Sealed is for Stop.
Consider a user management feature: the User entity, which requires a unique identifier, should be implemented as a class. The CreateUserRequest, serving as a data container, is best represented as a record. The IUserRepository defines required methods and is therefore an interface. The specific SqlUserRepository should remain internal to restrict visibility. Finally, the PasswordHasher should be sealed to prevent modification.
Applying this model helps avoid the "Equality Trap" and other common performance issues. The "Equality Trap" occurs when reference equality is mistakenly used for types that require value equality, or vice versa, leading to subtle bugs and unpredictable behaviour. Deliberate use of classes and records ensures equality semantics are explicit, maintaining a clean, comprehensible codebase for the entire team.
Conclusion
In sports, the advice to "get the basics right" is always true. In software engineering, these fundamentals are the foundation of everything we build. Choosing a class or record, or sealing a class, shapes your entire architecture.
True progress in software development often stems from mastering existing tools rather than adopting new frameworks. I encourage you to review your current project and consider whether a module could be simplified by using a record instead of a complex class. Mastering these fundamentals will enhance your effectiveness as a developer. If you would like an external review of your project, I am available to provide a code audit.
Frequently Asked Questions
Can a record inherit from a class? No, a record cannot inherit from a class, and a class cannot inherit from a record. Records can only inherit from other records. This rule helps maintain the specific value-based equality and immutability for which records are designed.
Is a sealed class faster than a regular class? Yes, sealed classes can be slightly faster because the JIT compiler can perform optimisations like devirtualization. It knows the class cannot be overridden, so it can call methods directly without checking a virtual method table.
When should I use an abstract class instead of an interface? Use an abstract class when you want to share common code or internal state between related types. Use an interface when you only need to define a contract of "what" a type can do without providing any implementation.
How do I test internal classes? You can test internal classes by using the InternalsVisibleTo attribute in your project file. This grants your unit test assembly access to the internal members while keeping them hidden from the rest of the world.
Are records always immutable? By default, positional records are immutable, but you can define mutable records if needed. However, immutability is generally preferred because it makes your code safer for multi-threaded applications.