Project: A Generic Repository
Project: A Generic Repository
Throughout this tutorial you have studied generics piece by piece: generic classes, bounded type parameters, wildcards, type erasure, and the rules around inheritance. In this final lesson you apply everything at once by building a Generic Repository — a reusable, type-safe in-memory store that can manage any entity you throw at it without duplicating a single line of persistence logic.
This pattern mirrors what real frameworks (Spring Data, Hibernate) do under the hood. Understanding it by hand makes those frameworks much less magical.
The Problem Without Generics
Imagine you need an in-memory store for a User and a separate one for a Product. Without generics you write two nearly identical classes, or you fall back to raw Object lists and lose all compile-time safety. Generics let you write the logic once and have the compiler enforce the correct type at every call site.
Step 1 — Define a Minimal Entity Contract
Every entity the repository manages must have a unique identifier. We express that contract with a bounded interface:
The bound ID extends Comparable<ID> means the repository can later sort or look up entities by their IDs using natural ordering — without knowing whether IDs are Long, String, or anything else.
Step 2 — Concrete Entity Classes
Java 17 records are ideal here: compact, immutable, and free of boilerplate. Notice that User uses a Long ID while Product uses a String SKU — the repository will handle both.
Step 3 — The Generic Repository Interface
Two type parameters: T — the entity type, bounded to Identifiable<ID> — and ID — the key type, bounded to Comparable<ID>. Every method is fully typed; no casting, no Object.
T extends Identifiable (raw), you would lose the ID type and be forced to cast when you call getId(). The second parameter ID threads the key type through every method signature, keeping everything safe.
Step 4 — The In-Memory Implementation
The implementation delegates to a LinkedHashMap (preserving insertion order). Returning Collections.unmodifiableList is a defensive copy pattern: callers get a snapshot, not a backdoor into the internal store.
Optional instead of returning null. findById returns Optional<T> so callers are forced to handle the "not found" case explicitly. This eliminates an entire class of NullPointerException bugs.
Step 5 — Extending the Repository With a Generic Method
A real repository often needs query capabilities. You can add a generic method that accepts a filter predicate, so the same class supports arbitrary searches without knowing what the entity looks like:
The wildcard Predicate<? super T> follows the PECS rule (Producer Extends, Consumer Super): a predicate consumes an entity to produce a boolean, so super is correct. This lets callers pass a Predicate<Object> or a Predicate<User> — both work.
Step 6 — Wiring It All Together
userRepo and productRepo are the same InMemoryRepository class — the generic parameters are erased. This is why you cannot do if (userRepo instanceof InMemoryRepository<User, Long>); the JVM only sees InMemoryRepository. The type safety exists at compile time only.
Design Takeaways
- One implementation, unlimited entity types.
InMemoryRepositorywill work for any class that implementsIdentifiable, with no changes. - Compile-time safety everywhere. The compiler rejects
userRepo.save(new Product(...))— no runtime surprises. - Readable API via
Optionaland streams. Callers write idiomatic Java without casting or null checks. - Open for extension. Subclasses can add domain-specific finders (e.g.,
UserRepository extends InMemoryRepository<User, Long>with afindByEmailmethod) without touching the base class.
Summary
You built a fully functional, type-safe generic repository from scratch: a bounded interface contract, two concrete entity records, a two-parameter generic interface, an InMemoryRepository implementation, and a generic filter method using PECS wildcards. Every concept from this tutorial — generic classes, bounded parameters, wildcards, and type erasure — contributed to the final design. This is the real power of generics: write logic once, let the compiler verify correctness at every use site, and never write a cast again.