Three years ago I wrote a blog post with the title Don’t use Optionals for data repositories. The post received a couple of critical comments and I had the feeling that I didn’t make my point clear.
This week I stumbled over the same topic again, but from a slightly different point of view. I designed some repository interfaces and got stuck on the question about how to name my methods. Here’s what I thought about.
A repository interface
Repository interfaces typically implement the same set of methods. For example the CRUD repository of Spring Data has amongst others the following methods:
public interface CrudRepository {
...
Optional findById(ID id);
Iterable findAll();
...
}
So is there anything wrong with these methods? No, not at all. But let’s compare it to another repository interface from Spring Data - the JPA repository:
public interface JpaRepository {
...
T getOne(ID id);
List findAll();
...
}
Also, this interface is perfectly fine. However, there’s a small but important difference between the two repositories: while the CRUD interface expects to find nothing, the JPA interface expects to always find something.
find vs. get
The difference between the two interfaces lies in the semantic of their methods.
The CRUD repository finds something whereas the JPA repository gets something.
While find
might lead to no result at all, get
will always return something - otherwise the JPA repository throws an exception.
The rules
This semantic leads to the following rules:
- Return an
Optional
if you try to find, search or look-up something. Finding nothing is an expected outcome in this case. The caller of the method must deal with it. - Return the entity or throw an exception if it doesn’t exist in case you reference it directly (get or load). In this case, it’s not a valid state if the entity does not exist - it’s an exception. However, by throwing an exception the caller doesn’t need to handle the inconsistency of the persistence layer.
- Return a (possibly empty) list in case of multiple elements in a result. In this case, it doesn’t matter if you have something or not (== the list is empty). It’s transparent for the caller.
- Never return
null
. It’s the at least meaningful return value you can choose.
Use cases
Having said that, what are suitable use cases for find
and get
?
Let’s make an example and assume that we’ve implemented a bookshop.
In this bookshop a user can search for a book by its title.
In this case, it’s perfectly fine that we don’t find anything.
The user might have made a typo or the book doesn’t exist.
So we would use something similar to Optional findById(ID id);
.
However, if we go one step further we run into another use case.
Let’s assume the user has found a book and made an order.
Eventually, we want to send an invoice to the user to get our money.
To do so, we load the order and look-up the book the user bought to get its price.
In this case, the book must exist! Otherwise, something is really broken.
So we would use something like T getOne(ID id);
.
Of course, a real world scenario would be much more complicated. The search would probably use a search index like Solar and the order would possibly have a foreign key on the book which ensures the integrity at the database level.
More
Best regards, Thomas.