There are a number of different options / approaches that are often taken:
Blocking like the API you've shown. This is a very simple API to use, and parallelism can still be achieved by invoking the API from within a multithreaded application.
Receiving/registering handlers in async operations. This is sometimes provided in combination with the blocking API (and, in fact, may be implemented in terms of the blocking API simply by spawning a background thread and then then invoking the handler at the end).
Returning a Future or ListenableFuture object, which makes the interface more idiomatically Java-esque (by returning data in the return-type position), but representing the eventual result, not an immediately available result. The Future can then be used to block or non-block.
Personally, my recommendation here would be:
interface EmployeeDatabase {
interface StringWhereClause {
ListQueryBuilder is(String value);
ListQueryBuilder contains(String value);
ListQueryBUilder matchesRegex(String regex);
}
interface IntWhereClause {
ListQueryBuilder is(int value);
ListQueryBuilder isInRange(int min, int max);
ListQueryBuilder isGreaterThan(int value);
ListQueryBUilder isLessThan(int value);
ListQueryBuilder isGreaterThanOrEqualTo(int value);
ListQueryBUilder isLessThanOrEqualTo(int value);
}
// ... matchers for other types of properties ...
interface ListQueryBuilder {
// Generic restrict methods
StringWhereClause whereStringProperty(String propertyName);
IntWhereClause whereIntProperty(String propertyName);
// ...
// Named restrict methods
StringWhereClause whereName();
StringWhereClause whereJobTitle();
IntWhereClause whereEmployeeNumber();
// ...
ListQueryBuilder limit(long maximumSize);
ListQueryBuilder offset(long index);
ResultSet<Employee> fetch();
}
ListQueryBuilder list();
ListenableFuture<Employee> getById(Key key);
ListenableFuture<KeyOrError> add(Employee employee);
ListenableFuture<Status> update(Key key, Employee employee);
ListenableFuture<Status> delete(Key key);
}
With:
interface ResultSet<T> {
Iterable<T> asIterable();
// ... other methods ...
}
interface KeyOrError {
boolean isError();
boolean isKey();
Key getKey();
Throwable getError();
}
interface Status {
boolean isOk();
boolean isError();
Throwable getError();
void verifyOk();
}
Basically, the idea is that insertion into the database returns a Key object (or an error if unsuccessful). This key can be used to retrieve, delete, or update the entry in the database. These operations (add, update, delete, and getById) all have a single result, in which case a ListenableFuture<T>
is used instead of type T
; this future object allows you to block (by calling .get()
on the future object) or to retrieve the object asynchronously (by registering a callback to be invoked when the result is ready).
For list-y operations, there are many different ways that a list can be filtered, subselected, sorted, etc. In order to prevent a combinatoric explosion of various different overloads, we use the builder pattern to allow these different restrictions to be applied in multiple combinations. In short, the builder interface provides a way to tack on zero or more options (sorts, filters, limits, etc.) to apply the retrieval operation, before calling fetch()
which causes the list query to be executed and returns a ResultSet
. This operation returns a ResultSet
rather than a ListenableFuture
, because the results are not all returned at once (e.g. they may come back from the data base in streaming fashion); the ResultSet
is effectively an interface with a similar behavior to a ListenableFuture
but for lists of items (where items may be ready at different times). For convenience, it is important to have a way to easily iterate over the contents of the ResultSet (e.g. by providing an Iterable
adapter to the ResultSet); however, you will probably also want to add other methods that allow you to perform other types of asynchronous processing on the ResultSet; for example, you might want a ListenableFuture<T> reduce(T initialValue, ReduceFunction<T> reducer)
to aggregate the elements in the result set and provide a future object representing the eventual completion of that.