Resource<T>
Runnable example:
ResourceSample.java
What is Resource<T>?
Resource<T> models a value that must be acquired before use and released afterward.
It is the composable alternative to Java’s try-with-resources statement.
Acquisition and release are declared together at construction time.
The resource is only live during the execution of use(fn) — it is acquired just before the
body runs, and the release function is always called when the body completes, whether it
succeeds or throws.
The result is returned as a Try<R>, making both success and failure explicit values.
Creating instances
// Explicit acquire + releaseResource<Connection> conn = Resource.of( () -> dataSource.getConnection(), Connection::close);
// AutoCloseable — close() is used as the release functionResource<BufferedReader> reader = Resource.fromAutoCloseable( () -> new BufferedReader(new FileReader(path)));Both of and fromAutoCloseable reject null arguments immediately with a
NullPointerException that names the offending parameter.
Using a resource
Resource<Connection> conn = Resource.of( () -> dataSource.getConnection(), Connection::close);
// Resource is acquired, body runs, resource is always releasedTry<List<User>> users = conn.use(c -> fetchUsers(c));
// Result is a Try — success or failure, release always happensusers.onSuccess(list -> System.out.println("Users: " + list.size())) .onFailure(ex -> System.err.println("Failed: " + ex.getMessage()));Each call to use() is independent: it acquires the resource, runs the body, and releases the
resource. The same Resource<T> can be used multiple times — each call goes through the full
acquire/run/release cycle.
Exception-merging contract
Resource<T> follows the same exception-suppression semantics as try-with-resources:
| Body | Release | Outcome |
|---|---|---|
| Success | Success | Try.success(result) |
| Success | Throws | Try.failure(releaseException) |
| Throws | Success | Try.failure(bodyException) |
| Throws | Throws | Try.failure(bodyException) — release exception suppressed onto body |
RuntimeException bodyEx = new RuntimeException("query failed");RuntimeException releaseEx = new RuntimeException("close failed");
Resource<Connection> conn = Resource.of( () -> openConnection(), c -> { c.close(); throw releaseEx; } // release always runs, but it may throw);
// Body throws, release also throws → release is suppressed onto bodyTry<Void> result = conn.use(c -> { throw bodyEx; });
Throwable cause = result.getCause();System.out.println(cause.getMessage()); // query failedSystem.out.println(cause.getSuppressed()[0].getMessage()); // close failedTransformations
map(fn)
Transforms the resource value without changing acquire/release.
If fn throws, the underlying resource is still released.
Resource<Connection> conn = Resource.of( () -> dataSource.getConnection(), Connection::close);
// Transform the resource value — acquire/release lifecycle is unchangedResource<String> schema = conn.map(c -> c.getSchema());
Try<Integer> len = schema.use(String::length);// Connection was opened, getSchema() was called, connection was closedmapTry(fn)
Like map, but the mapping function returns a Try<R>.
Useful when the transformation is itself a fallible operation already wrapped in a Try
(e.g., parsing, validation, or a Try.of(...)-wrapped API call).
If fn returns a failure, the underlying resource is still released.
// rawText resource acquires a file and reads its contentResource<String> rawText = Resource.fromAutoCloseable( () -> new BufferedReader(new FileReader(configPath))).map(reader -> reader.lines().collect(joining("\n")));
// mapTry chains a Try-returning function — if parsing fails, the resource is still releasedResource<Config> configResource = rawText.mapTry(text -> Try.of(() -> Config.parse(text)));
Try<Integer> port = configResource.use(Config::port);flatMap(fn)
Sequences two resources. The inner resource is derived from the outer resource’s value. Both resources are released in reverse acquisition order: inner first, then outer.
Resource<Connection> connResource = Resource.of( () -> dataSource.getConnection(), Connection::close);
// Sequence two resources — inner depends on outerResource<PreparedStatement> stmtResource = connResource.flatMap(conn -> Resource.of( () -> conn.prepareStatement("SELECT * FROM users"), PreparedStatement::close ));
// Acquire order: Connection → PreparedStatement// Release order: PreparedStatement → Connection (reverse)Try<List<User>> users = stmtResource.use(ps -> mapRows(ps.executeQuery()));This mirrors nested try-with-resources blocks but composes as a value,
so you can build resource pipelines without deep nesting.
Resource vs try-with-resources
try-with-resources | Resource<T> | |
|---|---|---|
| Guaranteed release | Yes | Yes |
| Composable | Nested blocks only | map / flatMap as values |
| Result as value | No — exceptions propagate | Yes — Try<R> or Result<R,E> returned |
| Multiple resources | Separate try blocks | flatMap chain |
| Reusable | No | Yes — each use() is independent |
| Works with non-closeable | No — requires AutoCloseable | Yes — any acquire/release pair |
Use Resource<T> when you need to compose multiple resources, reuse the acquire/release
description across multiple call sites, or integrate with the dmx-fun type system.
Use try-with-resources for simple, local, one-off resource management.
Interoperability
Resource.eval(Try<T>, release) — factory from a pre-computed Try
Use this when acquisition has already been attempted and the result is a Try<T>.
If the Try is a failure, use returns that failure immediately and release is never called.
// You already have a Try<Connection> from wrapping legacy codeTry<Connection> tryConn = Try.of(() -> dataSource.getConnection());
// Lift it into a Resource — release is skipped if acquisition already failedResource<Connection> conn = Resource.eval(tryConn, Connection::close);
Try<List<User>> users = conn.use(c -> fetchUsers(c));// If tryConn was a failure, users carries that failure and Connection::close is never calleduseAsResult(body, onError) — Result-integrated execution
The Result-counterpart of use(). The body returns a Result<R, E> directly.
Any Throwable from acquire, release, or an unexpected body exception is mapped to E via
onError, eliminating the Try<Result<R, E>> nesting.
sealed interface DbError { record QueryFailed(String message) implements DbError {} record ConnectionFailed(String message) implements DbError {}}
Resource<Connection> connResource = Resource.of( () -> dataSource.getConnection(), Connection::close);
// Body returns Result<R, E> — acquire/release Throwables are mapped to E via onErrorResult<List<User>, DbError> users = connResource.useAsResult( conn -> fetchUsers(conn), // returns Result<List<User>, DbError> ex -> new DbError.ConnectionFailed(ex.getMessage()));
// No Try wrapping needed — result is already in the domain error typeswitch (users) { case Ok<List<User>, DbError> ok -> ok.value().forEach(System.out::println); case Err<List<User>, DbError> err -> System.err.println("DB error: " + err.error());}Real-world example
A database query that composes a Connection and a PreparedStatement:
// Acquire a connection and a prepared statement, run a query, release bothResource<Connection> connResource = Resource.of( () -> dataSource.getConnection(), Connection::close);
Resource<PreparedStatement> stmtResource = connResource.flatMap(conn -> Resource.of( () -> { PreparedStatement ps = conn.prepareStatement( "SELECT id, name FROM orders WHERE customer_id = ?"); ps.setLong(1, customerId); return ps; }, PreparedStatement::close ));
Try<List<OrderSummary>> result = stmtResource.use(ps -> { List<OrderSummary> rows = new ArrayList<>(); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { rows.add(new OrderSummary(rs.getLong("id"), rs.getString("name"))); } } return rows;});
// Both resources are released regardless of whether the body succeededresult .onSuccess(orders -> orders.forEach(System.out::println)) .onFailure(ex -> log.error("Query failed", ex));