C# Generics — Boxing-Induced OutOfMemory in Trading Engines
ArrayList boxing in a trading engine caused OutOfMemory every 2 hours (30% GC CPU).
20+ years shipping production .NET services in enterprise systems. Drawn from code that ran under real load.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- Generics let you parameterize types with placeholders (T) for compile-time safety.
- List
replaces ArrayList — no runtime casts, no boxing for value types. - Constraints (where T : IInterface) unlock member access on T.
- Covariance (out) and contravariance (in) enable type-safe substitution on interfaces.
- Performance: value types avoid boxing, cutting GC pressure by up to 90% in hot paths.
- Biggest mistake: assuming T can do anything without constraints — expect compile errors.
Imagine you own a vending machine that only accepts one type of coin — you'd need a separate machine for quarters, dimes, and nickels. That's what non-generic code feels like. Generics let you build ONE vending machine with a adjustable slot that you lock to a specific coin type when you need it. The machine's logic stays the same; you just tell it upfront what type of coin it'll be dealing with. No guessing, no fumbling, no wrong coins jamming the mechanism.
Generics are everywhere in C#. List<T>, Dictionary<TKey, TValue>, Task<T> — you use them daily without thinking about what's happening under the hood. That's fine until something breaks. A cast fails. Boxing spikes your GC. A constraint you didn't add forces a runtime workaround. That's when knowing how generics actually work saves you a late-night debug session.
Before C# 2.0, developers used ArrayList and cast everything to and from object. The compiler couldn't catch type mismatches — a bug that should fail at compile time exploded as InvalidCastException at runtime. Generics fix this by letting you parameterise a class with a type placeholder. The compiler fills it in when you use the code, giving you full type checking without any runtime casting overhead.
By the end of this article you'll understand why generics exist at a language-design level, write your own generic classes with constraints, combine generics with interfaces for real architectural patterns, and avoid the three mistakes that trip up even experienced developers. You'll also walk away with sharp answers to the interview questions that separate juniors from mid-level engineers.
What Generics Actually Prevent — and Why Trading Engines Care
Generics in C# let you define classes, methods, and interfaces with a placeholder type that is specified at compile time. The core mechanic: the compiler generates specialized code for each value type used, eliminating boxing and unboxing overhead. For reference types, it shares a single internal instantiation via runtime trickery, but for structs — like decimal, int, or custom value types — each gets its own native code path.
In practice, this means a List stores integers directly in a contiguous array, not as boxed objects on the heap. A List storing ints boxes each value: allocation, pointer dereference, GC pressure. The difference is measurable: a tight loop over 10 million ints in a generic list runs at native speed; the boxed version allocates 80 MB of heap objects and triggers multiple Gen-2 collections.
Use generics whenever you need type-safe collections, algorithms, or data structures that work with value types. In trading engines, where latency is measured in microseconds and memory churn kills determinism, generics are not optional — they are the difference between a predictable 10μs path and a GC pause that blows your P99.
ArrayList for price levels. Each decimal addition boxed to object, allocating 16 bytes per level. With 10,000 levels updated 100 times per second, the process consumed 1.5 GB/min of heap — causing OOM crashes every 90 seconds.object in the type signature, you are paying for boxing. Switch to a generic collection immediately.List<T>, Dictionary<TKey, TValue>, and custom generic structs over ArrayList or Hashtable.The Problem Generics Solve — Why object-Based Code Is a Time Bomb
Before you can appreciate generics, you need to feel the pain they eliminate. The classic approach before C# 2.0 was to write everything against the object type — the root of all C# types. It seemed clever: one method handles everything. In practice, it was a maintenance nightmare.
Every value you pulled out had to be cast back to its real type. The compiler had no idea what was actually in your collection. You could put a string in a list of integers and the code would compile fine — it would just blow up at runtime when some unsuspecting method tried to call .ToString() on what it assumed was an int.
There's also a performance cost. Value types like int and double must be 'boxed' — wrapped in a heap-allocated object — to be stored as object, then 'unboxed' when retrieved. In tight loops processing thousands of items, this garbage pressure is measurable.
Generics eliminate both problems. You declare the type once at the use site, the compiler enforces it everywhere, and value types are stored directly without boxing. You get the flexibility of writing reusable code AND the safety of a strongly-typed language — not a trade-off between them.
Writing Your Own Generic Class — Building a Type-Safe Result Wrapper
The best way to deeply understand generics is to build something you'd actually use in production. A Result
The T in Result
Notice how the class is defined once, but can hold a string result, an int result, or a complex User object result. The internal logic — storing the value, checking success, returning errors — is written exactly once. That's the core promise of generics: write the shape of the behaviour, defer the type decision to the caller.
The private constructor pattern combined with static factory methods also means you can never accidentally create a Result
Generic Constraints — Teaching the Compiler What T Can Do
Here's the most powerful — and most misunderstood — feature of C# generics: constraints. Without them, T is a complete mystery to the compiler. It could be anything, so you can only call the methods that every single type in C# shares: ToString(), GetHashCode(), and Equals(). That's a pretty short list.
Constraints let you tell the compiler 'T is guaranteed to be at least this kind of thing'. Once you add a constraint, the compiler unlocks every method and property defined by that constraint. You get IntelliSense, type checking, and zero casting.
The where keyword is how you add constraints. The most common ones are: where T : class (T must be a reference type), where T : struct (T must be a value type), where T : new() (T must have a parameterless constructor), and where T : ISomeInterface (T must implement that interface). You can combine multiple constraints on the same type parameter.
The interface constraint is the one you'll use most in real codebases. It's how you write algorithms that are generic over behaviour, not type. A sorting method that works on anything sortable, a repository that works on anything with an ID — these are built with interface constraints.
ToString() on T works without constraint, but .CompareTo() does not.Generic Interfaces and Covariance — The Pattern Behind LINQ and IEnumerable
Once you're comfortable writing generic classes, the next level is understanding generic interfaces and variance — specifically covariance (out) and contravariance (in). These aren't academic features; they're why you can assign a List<string> to an IEnumerable<string> variable, and why LINQ works seamlessly across all collection types.
Covariance means a generic type with a more derived type argument can be treated as a generic type with a base type. So IEnumerable<string> can be assigned to IEnumerable<object> because string derives from object, and IEnumerable<T> is declared with out T — meaning T is only ever produced (returned), never consumed. The out keyword is what tells the compiler it's safe to widen the type.
Contravariance is the reverse — Action<object> can be assigned to Action<string> because Action<T> uses in T, meaning T is only consumed (taken as input). If you can handle any object, you can certainly handle a string.
In practice, you'll consume covariant and contravariant interfaces far more often than you'll write them. But knowing WHY IEnumerable<T> uses out T explains why so much LINQ code just works, and it's the kind of deep knowledge that separates engineers who use the framework from those who understand it.
Generic Methods and Type Inference — When the Compiler Deducing T Works (and When It Doesn't)
Generic methods are distinct from generic classes. You can have a generic method inside a non-generic class, and the type parameter is inferred from the arguments you pass. This is incredibly convenient — you don't need to specify the type unless the compiler can't figure it out. For example, when you write var result = Helper.Swap(ref a, ref b); the compiler infers T from the type of a.
But type inference has limits. If the method's type parameter appears only in the return type, the compiler cannot infer it — you must specify it explicitly. This is common in factory patterns: T Createnew() requires you to call Create
Another common gotcha is overload resolution. If two overloads differ only by a generic type parameter, the compiler may pick the wrong one or fail with an ambiguity error. In that case, explicitly specifying the type argument resolves the ambiguity.
Understanding when inference works and when it doesn't separates developers who fight the compiler from those who let it work for them.
Real-World Generic Patterns — Repository, Specification and Type-Safe Builders
Now that you understand constraints and variance, let's look at three real-world patterns that use generics to solve production problems. These aren't academic — they're patterns you'll find in every mature C# codebase.
The Generic Repository pattern keeps data access code consistent across entity types. With a constraint like where T : IEntity, you get a single implementation that handles Product, Order, User — any type with an identity. The pattern reduces duplication, but it also introduces a decision: do you build one grand repository or compose small ones? Generics let you do either.
The Specification pattern pairs with generics to build composable, testable query logic. A Specification<T> is a predicate wrapped in a class. You combine specifications with &&, ||, and ! operators. Pass them to a generic repository method: repository.Find(spec). The T makes the specification reusable across entity types without casting.
Type-safe Builders use generic methods to enforce a construction sequence at compile time. Instead of a builder that throws InvalidOperationException when you call Build() too early, you make each step return a new builder type. The compiler prevents you from creating an invalid object in the first place — no runtime checks needed.
- A non-generic class says: I work with Product. A generic class says: I work with anything that has an Id.
- Constraints define the minimum set of capabilities a type must have to work with your code.
- The more generic your code, the more constraints you need — otherwise the compiler can't guarantee anything.
- Every generic parameter is a trade-off: more flexibility means more complexity in understanding the code.
- The goal isn't maximum genericity — it's the right amount of genericity for your use case.
Generic Delegates — The Silent Assassin in Event-Driven Systems
Delegates without generics are null-checks waiting to happen. In trading engines, a price-feed handler that casts from object will eventually blow up when someone passes an OrderBook instead of a Quote. Generic delegates — Action<T>, Func<T, TResult>, Predicate<T> — force the contract at compile time. Your event publisher says 'I expect a Quote' and the compiler enforces it. No runtime casting, no reflection overhead. The pattern is brutal: define a delegate that matches your payload exactly, then wire it with generics. When the market data feed changes schema, the compiler breaks every mismatched subscriber before CI finishes. That's not paranoia; that's production discipline. The junior who wires a multicast delegate with object is the same junior who gets paged at 3 AM because a JSON parser returned a different type than expected.
Generic Constraints — The Nine Words That Saved a Weekend
Constraints aren't compiler pedantry; they're contracts that prevent runtime explosions. Without constraints, your generic repository's GetById<T> will happily accept T = StringBuilder, then fail when you try to cast it to an entity. The 'where T : class' constraint says 'only reference types'. The 'where T : new()' constraint promises the default constructor exists. Stack them: 'where T : class, IEntity, new()'. Now your generic cache actually works because every T has an Id property and a parameterless constructor. The junior who says 'I'll add constraints later' is the same junior who ship a generic serializer that throws on structs. In .NET 8, constraints also enable static abstract interface methods — your generic math finally compiles. 'where T : INumber<T>' unlocks Add, Subtract, Multiply without boxing. That is the difference between latency-critical code and toy examples.
Generic Math Interfaces INumber (C# 11+)
C# 11 introduced generic math interfaces, enabling you to write generic algorithms that work with numeric types. The INumber<T> interface (and related interfaces like IAdditionOperators<T, T, T>) allows you to constrain generic type parameters to numeric types, eliminating the need for boxing or runtime type checks. For example, you can create a generic Sum method that works with int, double, decimal, or any type implementing INumber<T>. This is particularly valuable in trading engines where high-performance numeric computations are common. Without these interfaces, you'd either need to overload methods for each numeric type or use object and boxing, which causes allocations and GC pressure. With INumber<T>, the compiler generates efficient, type-specific code without boxing. The interfaces are part of the System.Numerics namespace and require .NET 7 or later. They also support operators like +, -, *, /, and comparisons, making them ideal for generic math libraries. However, note that these interfaces are not available in older .NET versions, so you may need to target .NET 7+ or use polyfill packages. In trading systems, using INumber<T> can reduce latency by avoiding boxing and enabling JIT optimizations like inlining and vectorization.
Covariant Return Types on Overrides
C# 9.0 introduced covariant return types, allowing an overriding method to return a more derived type than the base method. This is particularly useful in generic hierarchies where you want to preserve type specificity without casting. For example, if you have a base class Repository<T> with a method GetById(int id) returning T, a derived CustomerRepository can override it to return a more specific type like Customer (which is T anyway), but more importantly, if you have a non-generic base with a virtual method returning object, you can override it to return a concrete type. This reduces the need for runtime type checks and casting, which can cause boxing if the return type is a value type. In trading engines, where value types like Price or Quantity are common, covariant return types help avoid boxing when overriding methods. For instance, a base interface IOrderProcessor might have a method Process() returning object, but a concrete implementation can override it to return OrderResult (a struct), eliminating boxing. This feature works with both classes and interfaces, but interfaces require explicit implementation. Note that covariant return types are not the same as generic covariance (which uses out); they are a simpler mechanism for method overrides. They improve type safety and performance by removing unnecessary casts and boxing.
Generic Attributes in C# 11
C# 11 introduced generic attributes, allowing you to define attributes that accept type parameters. Previously, attributes could only take Type as a parameter, requiring runtime reflection to get the type. With generic attributes, you can specify the type at compile time, enabling better type safety and eliminating boxing when storing type information. For example, you can create a [Validator attribute that validates a property against a specific type. In trading engines, this is useful for metadata-driven systems like serialization, validation, or mapping. Without generic attributes, you'd need to pass a Type object, which often involves boxing if the type is a value type. With generic attributes, the type parameter is preserved without boxing. However, generic attributes have limitations: they cannot be used on attributes that are applied to generic type parameters themselves, and they must be used with concrete types (not open generics). Also, the attribute class must be unsealed to allow inheritance? Actually, generic attributes can be sealed or unsealed. They work with reflection, but you need to use GetCustomAttribute with the constructed generic type. For example, typeof(MyClass).GetCustomAttribute. This feature is part of .NET 7+ and C# 11. In trading systems, generic attributes can reduce runtime overhead by moving type checks to compile time, and they avoid boxing when storing value type information in attributes.
Boxing-Induced OutOfMemory in a High-Frequency Trading Engine
- Value types in non-generic collections cause boxing overhead in every read and write.
- When profiling shows high GC, check collections for boxing — it's the silent killer.
- Generics are not just a safety feature; they are a performance requirement in hot paths dealing with value types.
ToList() or use .ElementAt(). Beware of multiple enumeration — cache to list if iterating more than once.ToList() or use .ConvertAll().dotnet-counters monitor --counters System.Runtime --process-id <pid>dotnet-dump collect --process-id <pid> then analyze with dotnet-dump analyze| File | Command / Code | Purpose |
|---|---|---|
| BeforeAndAfterGenerics.cs | using System; | The Problem Generics Solve |
| ResultWrapper.cs | using System; | Writing Your Own Generic Class |
| GenericConstraints.cs | using System; | Generic Constraints |
| VarianceAndGenericInterfaces.cs | using System; | Generic Interfaces and Covariance |
| GenericMethodsInference.cs | using System; | Generic Methods and Type Inference |
| RealWorldPatterns.cs | using System; | Real-World Generic Patterns |
| MarketDataFeed.cs | public delegate void QuoteHandler | Generic Delegates |
| EntityCache.cs | public interface IEntity { int Id { get; } } | Generic Constraints |
| GenericMathExample.cs | using System.Numerics; | Generic Math Interfaces INumber |
| CovariantReturnExample.cs | public abstract class OrderBase | Covariant Return Types on Overrides |
| GenericAttributeExample.cs | public class ValidatorAttribute | Generic Attributes in C# 11 |
Key takeaways
Interview Questions on This Topic
What is the difference between a generic constraint 'where T : class' and 'where T : IMyInterface', and when would you choose one over the other?
new()'.Frequently Asked Questions
20+ years shipping production .NET services in enterprise systems. Drawn from code that ran under real load.
That's OOP in C#. Mark it forged?
9 min read · try the examples if you haven't