When to use IList and when to use List
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Choosing between IList<T> and List<T> in C# is mostly a design decision, not a syntax decision. Both represent ordered collections, but they signal different intent to readers of your code and to callers of your API. If you choose carefully, your code is easier to test, easier to refactor, and less likely to be locked into one implementation detail.
Design Boundary: Interface Versus Implementation
IList<T> is an interface contract. List<T> is one concrete implementation of that contract. That difference matters most at boundaries such as service methods, repository abstractions, and public library APIs.
When you accept IList<T> as a parameter, you tell callers that your method needs list-like behavior such as indexing, insert, remove, and count, but does not require a specific backing type. The caller can pass a regular List<T>, a custom list wrapper, or a test double. This reduces coupling and improves testability.
When you return IList<T>, you hide the concrete type and keep room for future implementation changes. You can later swap to another list implementation without breaking callers that depend only on the interface. This is useful in layered applications where you want a stable contract between layers.
You should still use List<T> inside your own implementation where concrete features are useful. List<T> gives direct access to operations such as Capacity, Sort, BinarySearch, and AsReadOnly. Those are practical tools when performance tuning or when you need specific behavior that is not part of IList<T>.
Practical Performance and Feature Tradeoffs
A common mistake is assuming that using IList<T> gives performance gains. It does not. Performance depends on the concrete implementation passed at runtime. If runtime behavior depends on array-backed characteristics, you still need to reason about the actual object instance.
In business code, the common and pragmatic pattern is this: use List<T> for local data construction and efficient manipulation, then expose an interface type to callers. This gives practical performance while keeping dependency surfaces clean.
Another pragmatic detail is mutation control. If a method should not mutate caller data, do not accept IList<T> and then mutate it accidentally. Accept IReadOnlyList<T> when only indexing and count are needed. Use IEnumerable<T> when only forward iteration is required. This communicates intent more accurately than defaulting to IList<T> everywhere.
The code above deliberately uses List<T> internally because it relies on Sort and pre-allocated capacity. At the same time, callers of a higher-level service can still receive IList<T> or IReadOnlyList<T> to avoid unnecessary coupling.
Common Pitfalls
- Treating
IList<T>as automatically immutable.IList<T>is mutable by design, so callers can still modify data unless you enforce read-only contracts. - Returning
List<T>from public APIs everywhere. This leaks implementation details and makes refactoring harder. - Accepting
IList<T>when only enumeration is needed. In that case,IEnumerable<T>is a better contract. - Forgetting defensive copies in boundary code. If mutation is risky, create a new list instance before passing data across layers.
Summary
- Use
IList<T>at boundaries when you need list operations but not a specific implementation. - Use
List<T>internally when you need concrete features such as capacity control and sorting. - Prefer
IReadOnlyList<T>for read-only indexed access andIEnumerable<T>for pure iteration. - Choose contracts that communicate mutability intent clearly.
- Design for replaceable abstractions while keeping implementation performance practical.

