C#
programming
interfaces
data structures
generics

CollectionT versus ListT what should you use on your interfaces?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When designing C# interfaces, choosing between Collection<T> and List<T> is usually the wrong first question. Both are concrete classes, while interface design is supposed to express required behavior, not leak implementation choices. In most public contracts, the best answer is actually “use neither unless you truly need that exact concrete type.”

Start From Behavior, Not Storage

Ask what the caller or implementer must be able to do.

  • iterate through items
  • know the count
  • index into the sequence
  • mutate the collection
  • enforce custom insertion rules

Once those requirements are clear, the right abstraction is usually one of the collection interfaces rather than a class. For example:

csharp
1public interface IUserQueryService
2{
3    IReadOnlyList<UserDto> GetActiveUsers();
4}

This says the caller can read items by index, but it does not promise that the implementation literally stores them in a List<T>.

Why Exposing List<T> Is Usually a Smell

List<T> is a great implementation type. It is not usually a great public contract type. If you accept or return List<T> in an interface, you force callers and implementers to materialize that exact class even when another structure would have worked just as well.

csharp
1public interface IOrderProcessor
2{
3    void Process(IEnumerable<Order> orders);
4}

That signature is more flexible than this one:

csharp
1public interface IOrderProcessor
2{
3    void Process(List<Order> orders);
4}

The second version blocks arrays, query results, generators, and custom collections unless they are first copied into a List<T>. That is needless coupling.

Where Collection<T> Fits

Collection<T> is different from List<T> in purpose. It is mainly designed as a base class for custom collection types that need controlled mutation behavior through overridable methods such as InsertItem, SetItem, and RemoveItem.

csharp
1using System;
2using System.Collections.ObjectModel;
3
4public class PositiveIntCollection : Collection<int>
5{
6    protected override void InsertItem(int index, int item)
7    {
8        if (item < 0)
9        {
10            throw new ArgumentException("Value must be non-negative.");
11        }
12
13        base.InsertItem(index, item);
14    }
15}

That is a good reason to use Collection<T> internally or as part of a custom domain type. It is not a strong reason to expose Collection<T> directly in interface signatures.

Choose the Narrowest Useful Interface

A practical guideline for public contracts is:

  • use IEnumerable<T> when the method only needs to enumerate
  • use IReadOnlyCollection<T> when count matters
  • use IReadOnlyList<T> when indexing matters
  • use ICollection<T> or IList<T> only when mutation is truly part of the contract

Examples:

csharp
1public interface IInvoiceCalculator
2{
3    decimal Sum(IEnumerable<LineItem> items);
4}
5
6public interface ICartView
7{
8    IReadOnlyCollection<CartItem> GetItems();
9}
10
11public interface IPlaylist
12{
13    IReadOnlyList<Song> Tracks { get; }
14}

These contracts communicate capabilities much more precisely than returning a concrete List<T> everywhere.

Parameters and Return Types Usually Differ

It is common for input parameters to want broader types and return values to want slightly richer read-only types.

For example, a method that merely consumes a sequence should usually accept IEnumerable<T>. A method that returns a cached ordered set of results might reasonably return IReadOnlyList<T>.

csharp
1public interface ITagNormalizer
2{
3    IReadOnlyList<string> Normalize(IEnumerable<string> rawTags);
4}

That design keeps the parameter flexible and the return type expressive without exposing mutability.

Performance Concerns Rarely Justify Leaking List<T>

Developers sometimes expose List<T> because they believe abstractions are slower. In most application-level APIs, that is the wrong optimization target. The larger cost usually comes from materialization, copying, I/O, or database access, not from whether the signature said IEnumerable<T> or List<T>.

If a hot path truly needs a specific structure, optimize internally and benchmark it. Do not default public contracts to concrete classes just because they feel familiar.

When a Concrete Type Is Justified

There are exceptions. If the exact semantics of the concrete type matter to the contract, exposing it can be reasonable. That is rare, but possible in tightly controlled libraries. The burden is on the API designer to show why the concrete type is part of the meaning, not merely the current implementation.

For ordinary business interfaces, that bar is usually not met.

Common Pitfalls

  • Exposing List<T> in public interfaces when callers only need enumeration or read-only access.
  • Using Collection<T> in a contract even though no custom collection behavior is required.
  • Choosing IList<T> or ICollection<T> by habit and unintentionally promising mutability.
  • Treating implementation convenience as though it were an API requirement.
  • Returning mutable collections from read-focused services and allowing accidental caller-side modification.

Summary

  • In most public C# interfaces, neither List<T> nor Collection<T> is the best contract type.
  • Start by identifying the behavior the contract actually needs to expose.
  • 'List<T> is usually an implementation detail, while Collection<T> is mainly useful for custom collection base classes.'
  • Prefer IEnumerable<T>, IReadOnlyCollection<T>, or IReadOnlyList<T> when they express the need more accurately.
  • Good collection contracts are narrow, behavior-focused, and easy to evolve.

Course illustration
Course illustration

All Rights Reserved.