Oop
Errors, Generics & Resources in Design
Three language features seen as design decisions. Exceptions are part of a class's contract: a failure reported by an ignorable return value gets lost, while a domain exception says what went wrong in business terms. Generics turn one component, such as a repository, into a type-safe one for every entity. And every resource and long-lived reference needs an owner, or files stay open and memory leaks. The Java language rules live in the Java guide; every example here runs in Java and Python, with verified output.
Suggest an editErrors, Generics & Resources in Design — Contracts, Reuse and Ownership
Exceptions, generics, file handling and garbage collection are usually taught as language features: how to write try, what the angle brackets in List<String> mean. Those language rules are taught in the Java guide: Exceptions, Generics, I/O, Files & NIO.2 and References, Equality & the Object Model.
This lesson asks the design questions those features help answer. How should a class tell the code that calls it that something went wrong? How do you write one component that works for many types of data? Which object is responsible for closing a file, or for letting go of an object it no longer needs?
💡 The core idea.
- Errors are part of a class's contract. If the calling code can ignore a failure, sooner or later it will. Report failures with exceptions named in the language of the business.
- Generics let one component work with many types, with the compiler checking each use.
- Every resource, and every reference that lives a long time, needs an owner: one object that is responsible for closing or releasing it.
This builds on Abstraction & Interfaces. Every output below was produced by running the code on Java 21 and Python 3.11.
You'll be able to: design how a class reports failure, and choose between returning a value and throwing a domain exception; write a generic component and say what the compiler checks for you; name the owner of each resource in a design, and spot references that keep objects alive too long.
📘 How to read the Intuition boxes. Each one is built in three moves:
- The mechanism — what the design does to the code that uses it.
- A concrete bite — a specific situation where the design choice pays off or hurts.
- The earned rule — the decision heuristic, now justified rather than asserted, plus its cost.
Table of contents
- Errors are part of the contract
- Generics: one component, many types
- Resources and references need an owner
- Mental-model summary
- Gotcha checklist
- Check yourself
- Sources
1. Errors are part of the contract
Here, a wallet reports a failed payment by returning false, and the booking code doesn't check the result:
Output:
ride booked and confirmed to the driverAnalysis. The payment failed, but the ride was confirmed anyway. The false reported the failure, but nothing forced the calling code to look at it. With a return value, every caller has to remember to check; nothing happens if one forgets.
A domain exception (an exception named after a business problem) cannot be ignored silently. Either the calling code handles it, or it travels up the call stack and stops the operation:
Output:
payment failed: need 250, have 100; ask the rider to top up 150Analysis. The booking stopped before the confirmation line, and the catch block had what it needed to recover: a message, and how much money was missing. The exception's name, InsufficientFundsException, describes the problem in business terms, so the calling code can handle this failure specifically, without catching every RuntimeException.
Intuition.
Mechanism. An exception jumps out of the normal flow of the program until some calling method catches it. Code that forgets to handle it gets a visible failure, not a silently wrong result. Java's checked exceptions go further: the compiler makes every calling method either catch them or declare that it throws them [1]. The language details, such as checked versus unchecked exceptions and try/catch/finally, are in the Java guide's Exceptions.
Concrete bite. Three common designs lose errors:
- Swallowing the exception:
catch (Exception e) {}turns a failure into a silent success. - Exposing the layer below: a
PaymentServicethat throwsSQLExceptionforces the code that calls it to know about its database. Catch theSQLExceptionand throw a domain exception instead, passing the original as its cause. - Returning
nullfor "not found": every caller must remember to check fornull. ReturnOptional<T>instead, or throw a domain exception if a missing value is an error.
💡 Earned rule. Treat the ways a method can fail as part of its signature. Use a domain exception for a failure the caller must not miss. Use Optional (or a result type) when "no value" is a normal outcome. Convert lower-level exceptions into domain exceptions where one layer calls another. Never swallow an exception.
The cost is designing a set of exception classes, and deciding where each one is caught. Retries, fallbacks and circuit breakers build on this in Dependency Injection & Error Handling.
2. Generics: one component, many types
A repository stores objects by their id. Without generics, you would write UserRepository, OrderRepository and so on, all nearly identical, or a single repository of Object whose results callers must cast. With generics, one class works for every type of object, and the compiler checks each use:
Output:
Ada
keyboard
users: 1, orders: 1Analysis. The same InMemoryRepository class stored users with String ids and orders with Long ids. users.findById("u1") returned a User, with no cast needed. In Java, saving an Order into the user repository is a compile-time error. Python records the same type parameters for type-checking tools such as mypy, but does not check them while the program runs.
Intuition.
Mechanism. A type parameter (T, ID) is a placeholder that each use of the class fills in. The compiler checks every call against the types filled in, and then removes them: at run time there is just one InMemoryRepository class [2]. Bounds (<T extends Comparable<T>>) and wildcards (List<? extends Shape>) are covered in the Java guide's Generics.
Concrete bite. With a repository of Object, a mistake like saving an order as a user compiles without complaint. It fails later, far from where it was made, with a ClassCastException when some other code casts what it reads back. Generics move that failure to the line that caused it, and to compile time.
💡 Earned rule. When the same logic works for many types (storage, caching, queues, splitting results into pages), write it once with type parameters, rather than once for each type or once for Object.
The cost is code that is more abstract and harder to read. Don't make a class generic until a second type needs it.
3. Resources and references need an owner
Two kinds of things last longer than a single method call, and must be released on purpose.
Resources: files, network connections, database connections, locks. The operating system, or a pool, only hands out a limited number of them. In a design, each resource needs exactly one owner: the object that opened it, and that must close it:
- When a resource is used inside one method, Java's
try-with-resources (Python'swith) closes it however the method ends, including by an exception [3]. - When an object keeps a resource open for its whole lifetime (a
FileLoggerholding an open file), that object should implementAutoCloseable(in Python,__enter__and__exit__), and its owner must close it. - A method that receives a resource as a parameter does not own it and must not close it.
References: an object stays in memory for as long as something still in use refers to it, because the garbage collector only frees objects that nothing can reach. So in Java, a "memory leak" is a reference that is kept after the object is no longer needed. The Java guide's References, Equality & the Object Model, section 6 shows when an object becomes unreachable. Common causes of leaks in designs:
| Leak | Why the objects stay alive | Fix |
|---|---|---|
| A cache with no size limit | the map refers to every entry forever | limit its size, and remove old entries (least recently used, or after a time limit) |
| Listeners that are never removed | the event source's list refers to every listener | unregister them when done; give each subscription an owner |
static collections |
a static field lives as long as the class | avoid static fields that change (Abstraction & Interfaces, section 4) |
| Long-lived objects holding short-lived ones | a session refers to every request it handled | store only ids, or copy just the data you need |
💡 Earned rule. For every resource, and every collection that grows, name its owner in the design and say when it is released. Open and close each resource in the same object, and give every cache a size limit.
The cost is deciding how long things live before you write the code. Leaks discovered in production cost much more.
4. Mental-model summary
| Principle | Consequence |
|---|---|
| A failure reported only by a return value gets ignored | Use exceptions for failures the caller must not miss |
| Domain exceptions name failures in business terms | Calling code can handle this failure, with the details it needs |
| Convert exceptions where one layer calls another | Calling code doesn't depend on your database or HTTP library |
Use Optional when "no value" is normal; never swallow exceptions |
No null checks to forget, and no silent successes |
| Generics: one component, checked by the compiler for each type | No copies for each type, no casts, and mistakes caught at compile time |
| Java removes type parameters after compiling; Python doesn't check them at run time | The checking happens in the compiler, or in a type-checking tool |
| Every resource has one owner that closes it | try-with-resources / with; long-lived holders are AutoCloseable |
| An object stays in memory as long as something refers to it | Caches without limits, forgotten listeners and static collections leak memory |
5. Gotcha checklist
| Symptom | Likely cause | Fix |
|---|---|---|
| An operation "succeeded", but its effect never happened | a false or error code was ignored, or an exception was swallowed |
throw a domain exception; never catch an exception and ignore it |
Callers catch SQLException in business code |
a lower layer's exception leaked through | catch it at the boundary; throw a domain exception with the cause |
NullPointerException far from the lookup that failed |
null returned for "not found" |
Optional<T>, or a domain exception |
ClassCastException reading from a collection |
an Object-typed or raw collection |
type parameters |
| "Too many open files" after hours of running | a resource without an owner that closes it | try-with-resources; AutoCloseable owners |
Memory grows steadily, then OutOfMemoryError |
an unbounded cache, listener list or static collection | bound it, evict, unregister |
✅ Check yourself
One check per objective. Answer before you open anything.
A UserService calls a UserDao that throws SQLException, and returns null when a user is missing. Redesign its error contract.
Inside UserService, catch SQLException and throw a domain exception such as UserStoreUnavailableException, passing the original exception as its cause so the full stack trace is kept. Return Optional<User> from findById, because a missing user is a normal outcome, not an error. Code that uses UserService then deals only with ideas about users (an empty Optional, or a user store that is unavailable) and never with JDBC.
📚 Sources
- The Java Language Specification, Java SE 21, §11.2 "Compile-Time Checking of Exceptions" — https://docs.oracle.com/javase/specs/jls/se21/html/jls-11.html#jls-11.2
- The Java Language Specification, Java SE 21, §4.6 "Type Erasure" — https://docs.oracle.com/javase/specs/jls/se21/html/jls-4.html#jls-4.6
- The Java Language Specification, Java SE 21, §14.20.3 "try-with-resources" — https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-14.20.3
🧪 Predict, then check.
- In section 1, remove the
try/catcharoundwallet.pay(250). Predict what is printed, and whether the confirmation line appears. - In section 2, uncomment
users.save(new Order(1002L, "mouse")). Predict the compiler's complaint. - In the Python version from section 2, add
users.save(Order(1002, "mouse"))and run it. Predict what happens, then runmypyon it if you have it.
Your Turn
Before you move on, check your understanding with the coach — explain the idea, apply it, weigh the trade-offs, then defend your reasoning.