Course contentsShow
← C#
Lesson 319 of 1,9083. Generics and Collections ArchitectureFree lesson

Architectural Evolution Patterns

Refactor legacy collection code into modern generic, high-performance architectures.

In this lesson, you will learn how to evolve legacy, non-generic collection architectures into modern, high-performance patterns without breaking existing system contracts.

The Evolution of Collection Architecture

When dealing with legacy codebases, you often encounter collections rooted in the non-generic ArrayList or Hashtable. These are architectural "anchors" that force boxing and unboxing, lead to unpredictable GC pressure, and lack type safety. The goal of architectural evolution is to transition these structures into generic, allocation-efficient containers while maintaining backward compatibility.

Transitioning to Generic Safety

The primary pattern for evolving these collections is the Adapter Pattern combined with a Facade. Instead of rewriting the entire system—which often leads to regression bugs—you wrap the legacy collection in a generic interface that enforces type safety while delegating the underlying storage to a modern, heap-friendly structure.

Consider a legacy data manager that returns ArrayList:

// Legacy: High GC pressure, boxing overhead
public ArrayList GetRecords() => _legacyData; 

// Evolved: Generic, type-safe, and cache-friendly
public IEnumerable<T> GetRecords<T>() 
{
    // Using a ReadOnly wrapper to protect underlying structure
    return _modernStorage.AsReadOnly();
}

Decoupling Logic from Storage

A sophisticated evolution pattern involves separating the iteration logic from the storage layout. By leveraging Struct-based Iterators and Span<T>, you can replace a legacy foreach loop (which allocates an enumerator object) with a zero-allocation ref struct enumerator.

When you evolve your architecture, you aren't just changing types; you are moving from Object-Oriented collections (where every item is a heap-allocated reference) to Data-Oriented collections (where data resides in contiguous memory).

As you transition, you should:

  1. Abstract the access: Use interfaces to hide the concrete implementation so you can swap an ArrayList for a List<T> or a Memory<T> buffer without the consumer knowing.
  2. Minimize Surface Area: Only expose the minimal methods required by the caller, using ReadOnly wrappers to prevent accidental state corruption during the transition.
  3. Control Capacity: Replace the implicit, often wasteful resizing of legacy collections with explicit Capacity Planning, ensuring that as you modernize, you avoid the Large Object Heap (LOH) fragmentation common in older, poorly sized implementations.

Key Takeaway: Architectural evolution involves wrapping legacy, heap-heavy structures in generic interfaces that transition internal storage toward contiguous, stack-friendly layouts without exposing implementation details to the consumer.