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 + release
Resource<Connection> conn = Resource.of(
() -> dataSource.getConnection(),
Connection::close
);
// AutoCloseable — close() is used as the release function
Resource<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 released
Try<List<User>> users = conn.use(c -> fetchUsers(c));
// Result is a Try — success or failure, release always happens
users.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:

BodyReleaseOutcome
SuccessSuccessTry.success(result)
SuccessThrowsTry.failure(releaseException)
ThrowsSuccessTry.failure(bodyException)
ThrowsThrowsTry.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 body
Try<Void> result = conn.use(c -> { throw bodyEx; });
Throwable cause = result.getCause();
System.out.println(cause.getMessage()); // query failed
System.out.println(cause.getSuppressed()[0].getMessage()); // close failed

Transformations

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 unchanged
Resource<String> schema = conn.map(c -> c.getSchema());
Try<Integer> len = schema.use(String::length);
// Connection was opened, getSchema() was called, connection was closed

mapTry(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 content
Resource<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 released
Resource<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 outer
Resource<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-resourcesResource<T>
Guaranteed releaseYesYes
ComposableNested blocks onlymap / flatMap as values
Result as valueNo — exceptions propagateYes — Try<R> or Result<R,E> returned
Multiple resourcesSeparate try blocksflatMap chain
ReusableNoYes — each use() is independent
Works with non-closeableNo — requires AutoCloseableYes — 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 code
Try<Connection> tryConn = Try.of(() -> dataSource.getConnection());
// Lift it into a Resource — release is skipped if acquisition already failed
Resource<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 called

useAsResult(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 onError
Result<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 type
switch (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 both
Resource<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 succeeded
result
.onSuccess(orders -> orders.forEach(System.out::println))
.onFailure(ex -> log.error("Query failed", ex));