Multiple screens displaying code in a developer workspace
Software Engineering May 21, 2026 • 15 min read

Generics and the Victory of Writing Code That Lasts Forever

You've written the same function three times for three different types. Generics fix that. Write once, use for any type, forever. This lesson introduces real syntax for the first time with C#, Java, and TypeScript side by side.

Share:
Lee Foropoulos

Lee Foropoulos

15 min read

Continue where you left off?
Text size:

Contents

Complete Guide | Lesson 8: Design PatternsLesson 9: Generics → Lesson 10: Normalization (May 28)

You've written the same sorting function three times: once for integers, once for strings, once for dates. Each one is identical except for the type. You copied and pasted because you didn't know a better way. Then the sorting logic had a bug, and you fixed it in one copy and forgot the other two.

There's a better way. There's always been a better way.

This is Lesson 9, and it maps to Netzach on the Tree of Life. Netzach means "Victory" or "Endurance." It's the force that sustains and repeats without exhaustion. Generics are code that endures because they work with any type. Write once, use with integers, strings, customers, invoices, or any type that hasn't been invented yet. The code doesn't care. It just works.

This is also the lesson where we break the language-agnostic barrier. For the first time in this series, you'll see the same concepts expressed in real syntax: C#, Java, and TypeScript side by side. Because generics prove the thesis of this entire series: the concept is universal. Only the notation changes.

The DRY Principle

DRY stands for Don't Repeat Yourself. Every piece of knowledge in a system should have a single, authoritative, unambiguous representation. When you duplicate code, you duplicate bugs. When you duplicate logic, you duplicate maintenance.

The progression from beginner to professional looks like this:

Level 1: Copy-paste. Need the same logic? Copy it. Now you have two copies. Change one, forget the other. Bugs multiply.

Level 2: Functions. Extract shared logic into a function. Call it from multiple places. One source of truth. But the function only works for one type.

Level 3: Generics. Write a function (or class) that works for ANY type. One source of truth, infinite applicability. Zero duplication.

Copy-paste programming isn't laziness. It's a time bomb. Every duplicated function is a promise that you'll remember to update all copies when the logic changes. You won't. Nobody does.
Multiple identical copies of a document spread across a desk
This is what a codebase looks like when you copy-paste instead of using generics. Five copies of the same logic. Change one. Forget the others. Ship a bug. Get paged at 3 AM.

Here's the concrete problem. You need a function that finds the maximum value in a list:

1// Without generics: you write it three times
2FUNCTION findMaxInt(list: List<Integer>): Integer
3    max = list[0]
4    FOR EACH item IN list
5        IF item > max THEN max = item
6    RETURN max
7
8FUNCTION findMaxString(list: List<String>): String
9    max = list[0]
10    FOR EACH item IN list
11        IF item > max THEN max = item
12    RETURN max
13
14FUNCTION findMaxDate(list: List<Date>): Date
15    max = list[0]
16    FOR EACH item IN list
17        IF item > max THEN max = item
18    RETURN max

Three functions. Identical logic. Three places to maintain bugs. With generics, you write it once:

1FUNCTION findMax<T>(list: List<T>): T WHERE T is Comparable
2    max = list[0]
3    FOR EACH item IN list
4        IF item > max THEN max = item
5    RETURN max
6
7// Works with ANY comparable type:
8findMax<Integer>([3, 7, 1, 9])     // Returns 9
9findMax<String>(["apple", "mango"]) // Returns "mango"
10findMax<Date>([jan1, mar15, feb28]) // Returns mar15
1 function
replaces 3 (or 30, or 300) identical copies when you use generics. Every new type works automatically. Zero new code. Zero new bugs.

Type Parameters: What the Angle Brackets Mean

The <T> syntax that scares beginners is simple once you understand it: T is a placeholder for a type that will be specified later.

List<T> means "a list of T, where T is whatever type you choose when you create the list."

  • List<int> = a list of integers
  • List<string> = a list of strings
  • List<Customer> = a list of Customer objects

The definition is written once with T. The caller specifies the actual type. The compiler enforces type safety for that specific usage.

Without Generics: The Dark Ages

Before generics, languages used untyped collections that stored everything as Object:

1// Java before generics (pre-2004):
2List numbers = new ArrayList();
3numbers.add(42);
4numbers.add("not a number");  // Compiles fine!
5
6int value = (int) numbers.get(1);  // Runtime crash! String isn't an int.

No compiler warning. No type safety. The bug hides until production.

With Generics: The Compiler Has Your Back

1// Java with generics:
2List<Integer> numbers = new ArrayList<>();
3numbers.add(42);
4numbers.add("not a number");  // COMPILER ERROR! String is not Integer.

The compiler catches the bug before the code ever runs.

First Real Syntax: Three Languages, One Concept

Here's where we prove that concepts transfer. The same generic list in three languages:

Generics Across Languages

C#:

csharp
List<int> numbers = new List<int>();
Dictionary<string, Customer> customers = new Dictionary<string, Customer>();

Java:

java
List<Integer> numbers = new ArrayList<>();
Map<String, Customer> customers = new HashMap<>();

TypeScript:

typescript
const numbers: Array<number> = [];
const customers: Map<string, Customer> = new Map();

Same concept. Different notation. If you understand the concept, the syntax is a five-minute lookup.

3 languages
same concept, different syntax. If you understand generics in one language, you understand them in all of them. That's why this series taught concepts first.

Generic Methods, Classes, and Interfaces

Generic Methods

A function that works with any type:

csharp
1// C#
2T FindMax<T>(List<T> items) where T : IComparable<T>
3{
4    T max = items[0];
5    foreach (T item in items)
6    {
7        if (item.CompareTo(max) > 0)
8            max = item;
9    }
10    return max;
11}
java
1// Java
2<T extends Comparable<T>> T findMax(List<T> items) {
3    T max = items.get(0);
4    for (T item : items) {
5        if (item.compareTo(max) > 0)
6            max = item;
7    }
8    return max;
9}
typescript
1// TypeScript
2function findMax<T>(items: T[], compare: (a: T, b: T) => number): T {
3    let max = items[0];
4    for (const item of items) {
5        if (compare(item, max) > 0) max = item;
6    }
7    return max;
8}

Three syntaxes. One idea. T is a placeholder. The constraint (IComparable, extends Comparable, or a comparison function) tells the compiler what T can do.

Generic Classes: The Repository Pattern

Here's the "aha moment" that makes senior developers fall in love with generics. Imagine you have a database with Customers, Orders, Products, and Invoices. Without generics, you write four data access classes:

1CLASS CustomerRepository
2    METHOD getById(id): Customer
3    METHOD getAll(): List<Customer>
4    METHOD add(customer: Customer)
5    METHOD update(customer: Customer)
6    METHOD delete(id)
7END CLASS
8
9CLASS OrderRepository
10    METHOD getById(id): Order
11    METHOD getAll(): List<Order>
12    METHOD add(order: Order)
13    METHOD update(order: Order)
14    METHOD delete(id)
15END CLASS
16
17// ...and ProductRepository, InvoiceRepository, etc.

Identical structure. Different types. Classic DRY violation. With generics:

1CLASS Repository<T>
2    METHOD getById(id): T
3    METHOD getAll(): List<T>
4    METHOD add(entity: T)
5    METHOD update(entity: T)
6    METHOD delete(id)
7END CLASS
8
9// One class serves all entities:
10customerRepo = new Repository<Customer>()
11orderRepo = new Repository<Order>()
12productRepo = new Repository<Product>()
13invoiceRepo = new Repository<Invoice>()
Single precision tool with interchangeable attachments for different tasks
One tool, many attachments. That's generics: one class definition that works with any type you attach to it. The Repository pattern is the most common real-world example.
One generic Repository replaces CustomerRepository, OrderRepository, ProductRepository, and every future entity repository you'll ever need. That's not just DRY. That's victory over duplication itself.
75%
reduction in data access code when switching from concrete repositories to a generic Repository pattern. In a project with 20 entity types, that's thousands of lines eliminated.

Generic Interfaces

Combine generics with interfaces (Lesson 6) for maximum power:

csharp
1// C#
2interface IRepository<T> where T : class
3{
4    T GetById(int id);
5    IEnumerable<T> GetAll();
6    void Add(T entity);
7    void Update(T entity);
8    void Delete(int id);
9}
10
11// Any implementation for any type:
12class SqlRepository<T> : IRepository<T> where T : class { ... }
13class InMemoryRepository<T> : IRepository<T> where T : class { ... }

Now you can inject IRepository<Customer> and the caller doesn't know whether it's talking to SQL Server, MongoDB, or an in-memory test double. Generics plus interfaces plus dependency injection (Lesson 6) equals maximum flexibility with maximum type safety.

Constraints: What T Can and Can't Do

An unconstrained T can be anything: an integer, a string, a Customer, a database connection. But "anything" means you can't do much with it. You can't compare it, sort it, or call methods on it, because the compiler doesn't know what methods T has.

Constraints narrow the possibilities and tell the compiler what T is capable of:

csharp
1// C# constraints
2T FindMax<T>(List<T> items) where T : IComparable<T>    // T must be comparable
3void Save<T>(T entity) where T : class                   // T must be a reference type
4T Create<T>() where T : new()                            // T must have parameterless constructor
5void Process<T>(T item) where T : ISerializable, new()   // Multiple constraints
java
1// Java constraints
2<T extends Comparable<T>> T findMax(List<T> items)       // T must implement Comparable
3<T extends Serializable & Closeable> void process(T item) // Multiple bounds
typescript
1// TypeScript constraints
2function process<T extends HasId>(item: T): void         // T must have an id property
3function merge<T extends object>(a: T, b: Partial<T>): T // T must be an object

Constraint Cheat Sheet

"T must be comparable" → C#: where T : IComparable<T> | Java: <T extends Comparable<T>> | TS: <T extends Comparable> "T must be a class/object" → C#: where T : class | Java: <T extends Object> (default) | TS: <T extends object> "T must have a constructor" → C#: where T : new() | Java: use reflection | TS: <T extends { new(): T }> "T must implement interface" → C#: where T : IMyInterface | Java: <T extends MyInterface> | TS: <T extends MyInterface> Constraints are the guardrails. Without them, T is too generic to be useful. With them, T is exactly as flexible as you need.

"The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise." Dijkstra's principle applies perfectly to generics: List<T> is not vague. It's precisely "a list of exactly one type, enforced by the compiler."

Multiple screens displaying code in a developer workspace
The same concept in three languages. If you've followed this series from Lesson 1, you can read all three. The syntax is just notation. The thinking is universal.
Generics aren't about writing less code. They're about writing code that can't rot. Every duplicated function decays independently. A generic function has one source of truth and zero drift.
Lesson 9 Practice 0/6
How was this article?

Share

Link copied to clipboard!

You Might Also Like

Lee Foropoulos

Lee Foropoulos

Business Development Lead at Lookatmedia, fractional executive, and founder of gotHABITS.

🔔

Never Miss a Post

Get notified when new articles are published. No email required.

You will see a banner on the site when a new post is published, plus a browser notification if you allow it.

Browser notifications only. No spam, no email.

0 / 0