Learn Java Io Modern Io Resource Boundaries Part 022 Resource Boundary Api Design
title: Learn Java IO, Modern IO, Streams, Buffers, Resources, Serialization & Data Boundaries - Part 022 description: API design for Java IO boundaries: choosing Path, File, InputStream, Reader, ByteBuffer, Channel, Supplier<InputStream>, callbacks, resource ownership, replayability, and boundary contracts. series: learn-java-io-modern-io-resource-boundaries seriesTitle: Learn Java IO, Modern IO, Streams, Buffers, Resources, Serialization & Data Boundaries order: 22 partTitle: API Design for IO Boundaries tags:
- java
- io
- api-design
- boundary-design
- streams
- nio
- resource-management
- series date: 2026-06-30
Part 022 — API Design for IO Boundaries
1. Why This Part Matters
Java gives us many IO shapes:
PathFileURIInputStreamOutputStreamReaderWriterbyte[]ByteBufferFileChannelSeekableByteChannelReadableByteChannelWritableByteChannelSupplier<InputStream>- callbacks
- Java
Stream<T>
The difficult part is not knowing that these exist.
The difficult part is choosing the right one for an API boundary.
A poor IO API forces callers into unsafe behavior:
- accidental full materialization
- unclear close responsibility
- impossible retry
- hidden blocking
- charset ambiguity
- resource leaks
- incorrect ownership transfer
- poor testability
- impossible progress/cancellation
- incomplete security and validation boundaries
This part gives a decision framework.
2. The Core Principle
An IO type is not just a data type.
It encodes boundary semantics.
| Type | Hidden Semantic Question |
|---|---|
Path | Can the callee open the resource itself? |
InputStream | Is the data one-shot and caller-owned? |
byte[] | Is the whole payload already materialized? |
ByteBuffer | Who owns position/limit and mutation? |
Reader | Has charset decoding already happened? |
Channel | Do we need positional or non-blocking semantics? |
Supplier<InputStream> | Is the source replayable? |
| Callback | Should the callee own the resource lifecycle? |
The API should expose the semantic contract you actually need.
3. Decision Tree
This tree is not absolute. It is a starting point.
The real design comes from invariants.
4. Boundary Questions Before Choosing a Type
Before designing any IO method, answer these questions.
4.1 Size
- Is the payload bounded?
- What is the maximum size?
- Is the maximum trusted or attacker-controlled?
- Is the maximum enforced by this method or upstream?
4.2 Lifetime
- Who opens the resource?
- Who closes it?
- Can the method retain it after returning?
- Can the method process it asynchronously?
4.3 Replayability
- Can the source be read more than once?
- Can failure be retried?
- Does retry require a new stream?
4.4 Position
- Is the source sequential only?
- Does the API require random access?
- Should reading mutate the caller's position?
4.5 Encoding
- Is this binary or text?
- If text, which charset?
- Who handles malformed input?
4.6 Blocking
- Can the call block?
- Is blocking acceptable on caller's thread?
- Is timeout/cancellation required?
4.7 Partial State
- What happens if write fails halfway?
- Is partial output visible?
- Is the operation atomic?
4.8 Ownership
- Does the callee own the resource?
- Is ownership transferred?
- Is the caller still allowed to use it after the method returns?
5. Path as an API Boundary
Use Path when the callee should open and manage the file.
Report parseReport(Path path) throws IOException;
This is good when the method needs:
- metadata
- file size
- attributes
- multiple passes
- safe open options
- retries by reopening
- temporary file strategy
- atomic moves
FileChannel- memory mapping
- lifecycle ownership
Example:
static long countBytes(Path path) throws IOException {
try (InputStream in = Files.newInputStream(path)) {
return in.transferTo(OutputStream.nullOutputStream());
}
}
The method opens the stream and closes it.
5.1 Advantages
| Advantage | Explanation |
|---|---|
| Clear lifecycle | Method creates resource and closes it |
| Replayable | Method can reopen if needed |
| Metadata available | Can inspect attributes, size, owner, etc. |
| Correct open options | Method controls CREATE_NEW, TRUNCATE_EXISTING, etc. |
| Testable | Caller can use temp files |
5.2 Disadvantages
| Disadvantage | Explanation |
|---|---|
| Filesystem-specific | Cannot directly pass socket/request body |
| Less abstract | API tied to local or provider-backed filesystem |
| Race concerns | Path may change between checks and opens |
5.3 Good Use Cases
Use Path for:
- file import
- file export
- indexing local files
- safe replace
- archive creation
- checksum of local file
- mmap
- file metadata logic
- APIs that need to own resource lifecycle
5.4 Bad Use Cases
Avoid Path when:
- data may come from network or memory
- caller already owns an open stream
- source is one-shot
- content is not filesystem-backed
6. File as an API Boundary
java.io.File is older than NIO.
Modern Java APIs should usually prefer Path.
Path path = file.toPath();
File may still appear because:
- legacy libraries use it
- old APIs require it
- migration is incremental
For new API design, prefer:
void importFile(Path path)
over:
void importFile(File file)
Path better represents provider-backed filesystems and composes with Files, FileSystem, and NIO.
7. InputStream as an API Boundary
Use InputStream when the method consumes a sequential byte source that the caller already opened.
Document parse(InputStream input) throws IOException;
This means:
- method reads bytes sequentially
- input is probably one-shot
- caller usually owns closing unless documented otherwise
- method should not assume replayability
- method should not read beyond its boundary contract
7.1 Ownership Contract
Bad API because ownership is unclear:
void parse(InputStream input);
Better documentation:
/**
* Parses a document from {@code input}.
* The caller remains responsible for closing {@code input}.
* This method consumes bytes until EOF.
*/
Document parse(InputStream input) throws IOException;
Alternative if method owns close:
/**
* Parses and closes {@code input} before returning.
*/
Document parseAndClose(InputStream input) throws IOException;
But be careful: surprising close behavior can break callers.
7.2 Good Use Cases
Use InputStream for:
- HTTP request body
- socket stream
- process stdout
- uploaded file stream
- in-memory source wrapped as
ByteArrayInputStream - compressed stream input
- generic byte parser
7.3 Bad Use Cases
Avoid raw InputStream if the method needs:
- file metadata
- seek/random access
- multiple passes
- reliable retry
- file locking
- atomic file operations
- known file identity
In those cases, prefer Path or SeekableByteChannel.
8. OutputStream as an API Boundary
Use OutputStream when the method produces sequential bytes into a caller-provided sink.
void writeReport(OutputStream output) throws IOException;
This means:
- method writes bytes
- caller likely owns close
- method may flush if required by contract
- method should not close unless explicitly stated
- partial output may exist on failure
8.1 Should the Method Flush?
Usually:
- library methods should not close caller-owned streams
- flushing may be acceptable at a logical boundary
- excessive flushing hurts performance
Example:
void writeFrame(OutputStream out, Frame frame) throws IOException {
writeHeader(out, frame);
writeBody(out, frame);
// no close
}
If frame visibility requires flush:
void writeFrameAndFlush(OutputStream out, Frame frame) throws IOException {
writeFrame(out, frame);
out.flush();
}
Make it explicit in the method name or documentation.
9. Reader and Writer Boundaries
Use Reader/Writer when bytes have already crossed the charset boundary.
Config parseConfig(Reader reader) throws IOException;
This API says:
- parser operates on characters
- caller controls charset decoding
- parser does not know original bytes
- parser cannot validate byte-level encoding details
Use Reader when the format is text and the caller should choose charset.
Use Path + Charset when the parser should own opening and decoding.
Config parseConfig(Path path, Charset charset) throws IOException;
Avoid APIs that silently use the platform default charset.
Bad:
new FileReader(file)
Better:
Files.newBufferedReader(path, StandardCharsets.UTF_8)
9.1 Good Use Cases
Use Reader for:
- text parsers
- templating engines
- line-based readers
- CSV-like parsers where caller owns decoding
Use Writer for:
- text report generation
- template rendering
- structured text output
9.2 Boundary Caution
Once you accept Reader, you cannot:
- compute checksum of original bytes
- inspect BOM reliably
- recover exact original bytes
- apply byte-level limits unless caller enforces them
This is not bad. It is a contract.
10. byte[] as an API Boundary
Use byte[] when the payload is already materialized and bounded.
Message parse(byte[] bytes);
This is good for:
- small payloads
- cryptographic digests
- tests
- protocol frames with known maximum size
- immutable-ish input values
- in-memory caches
But byte[] communicates:
The whole payload is already in memory.
Do not use byte[] as the only API for potentially large data.
Bad:
void upload(byte[] file);
Better:
void upload(InputStream file, long contentLength);
or:
void upload(Path file);
10.1 Defensive Copying
Arrays are mutable.
If storing a byte[], consider copying:
record Blob(byte[] bytes) {
Blob {
bytes = bytes.clone();
}
@Override
public byte[] bytes() {
return bytes.clone();
}
}
For high-performance code, copying may be too expensive, but then ownership must be explicit.
11. String as an API Boundary
Use String only after text decoding and full materialization are intended.
Template compile(String source);
Good for:
- small config snippets
- SQL/query templates
- test fixtures
- small JSON/XML snippets already decoded
Bad for:
- huge files
- unknown-size request bodies
- binary data
- text where encoding errors matter
A String has no original charset, no original newline bytes, and no BOM.
12. ByteBuffer as an API Boundary
Use ByteBuffer when you need NIO-style buffer semantics:
- position/limit/capacity
- direct buffer compatibility
- channel IO
- binary parsing
- slicing
- zero-copy-ish views
- interop with APIs that expect buffers
Example:
int parseFrame(ByteBuffer buffer);
But ByteBuffer APIs are easy to design poorly because position is mutable.
12.1 State-Mutating Contract
This method consumes bytes by advancing position:
Frame readFrame(ByteBuffer buffer);
Document:
On success, buffer position is advanced past the frame. On failure, position is unspecified.
Or stronger:
On failure, buffer position is restored.
But restoring position requires deliberate code.
12.2 Non-Mutating Contract
Use duplicate/slice for read-only parsing:
Frame peekFrame(ByteBuffer buffer) {
ByteBuffer view = buffer.asReadOnlyBuffer();
return parseWithoutMutatingCallerPosition(view);
}
12.3 ByteBuffer Ownership Matrix
| API Contract | Meaning |
|---|---|
| borrows buffer during call | method must not retain reference |
| consumes buffer position | caller expects position mutation |
| retains buffer | caller must not mutate after call |
| copies buffer | safe but costs memory |
| returns slice | shared content, independent position/limit |
This must be explicit.
13. ReadableByteChannel and WritableByteChannel
Use channel interfaces when the API is byte-oriented and may integrate with NIO.
long transfer(ReadableByteChannel source, WritableByteChannel sink) throws IOException;
Good for:
- NIO ecosystem
ByteBuffer-based loops- non-stream source/sink abstraction
- adapters via
Channels.newChannel
Channel reads/writes may be partial.
while (buffer.hasRemaining()) {
sink.write(buffer);
}
For blocking channels this eventually drains or fails. For non-blocking channels, a zero write may require readiness management.
Do not design a channel API unless your implementation handles channel semantics correctly.
14. SeekableByteChannel and FileChannel
Use SeekableByteChannel when random access matters but you do not need FileChannel specifics.
Index readIndex(SeekableByteChannel channel) throws IOException;
Use FileChannel when you need:
- file locking
- memory mapping
forcetransferTo/transferFrom- file-specific operations
- positional read/write with stronger file semantics
Example:
void writeSegment(FileChannel channel, long offset, ByteBuffer data) throws IOException;
This contract is precise:
- target is a file channel
- operation writes at a position
- caller owns channel lifecycle
- caller controls durability unless method calls
force
15. Supplier<InputStream> for Replayable Data
Use Supplier<InputStream> when an operation may need to read the data more than once.
void upload(Supplier<? extends InputStream> bodySupplier) throws IOException;
But Supplier<InputStream> is incomplete unless documented.
Required contract:
/**
* The supplier must return a new stream positioned at the beginning for each call.
* The caller remains responsible for ensuring the underlying content is stable
* across retries.
*/
void upload(Supplier<? extends InputStream> bodySupplier) throws IOException;
Good use cases:
- retryable uploads
- checksum before transfer
- content-length calculation plus send
- signing then sending
- parsers needing multiple passes
Bad use:
InputStream in = Files.newInputStream(path);
Supplier<InputStream> bad = () -> in;
That returns the same consumed stream.
Good:
Supplier<InputStream> good = () -> Files.newInputStream(path);
16. Callback-Based APIs
Callbacks let the callee own resource lifecycle.
static <T> T withInputStream(Path path, IOFunction<InputStream, T> action) throws IOException {
try (InputStream in = Files.newInputStream(path)) {
return action.apply(in);
}
}
@FunctionalInterface
interface IOFunction<T, R> {
R apply(T value) throws IOException;
}
Usage:
long size = withInputStream(path, in -> in.transferTo(OutputStream.nullOutputStream()));
Benefits:
- resource cannot escape accidentally
- lifecycle is clear
- method controls open options
- caller provides behavior
Costs:
- more abstract
- harder to compose with some frameworks
- callback must not retain resource
Document:
The input stream is valid only during callback execution.
17. Visitor / Handler APIs for Records
For record-oriented streaming, do not expose List<Record> if records may be huge.
Bad:
List<Record> parse(Path path) throws IOException;
Better:
void parse(Path path, RecordHandler handler) throws IOException;
@FunctionalInterface
interface RecordHandler {
void onRecord(Record record) throws IOException;
}
This allows streaming processing.
But handler APIs need failure behavior:
- If handler throws, does parsing stop?
- Is source closed?
- Is partial output kept?
- Can handler request cancellation without exception?
A richer interface:
interface RecordHandler {
ProcessingDecision onRecord(Record record) throws IOException;
}
enum ProcessingDecision {
CONTINUE,
STOP
}
This makes early termination explicit.
18. Java Stream<T> as Boundary
Returning Stream<T> can be elegant but dangerous for IO-backed data.
Stream<Record> records(Path path) throws IOException;
Problems:
- stream must be closed
- exceptions inside stream pipelines are awkward
- resource lifetime crosses method boundary
- caller may process lazily after surrounding context ends
- parallel stream may be unsafe
If you return an IO-backed Java stream, document closure:
/**
* Returns a lazily read stream of records. The caller must close the stream.
*/
Stream<Record> records(Path path) throws IOException;
Usage must be:
try (Stream<Record> records = reader.records(path)) {
records.forEach(this::process);
}
For internal platform APIs, callback style is often safer.
19. Content Length and Metadata
Sometimes data alone is insufficient.
An input stream has no inherent size.
Avoid APIs that need size but accept only InputStream.
Bad:
void upload(InputStream body);
Better:
void upload(InputStream body, OptionalLong contentLength);
or:
record BodySource(
Supplier<? extends InputStream> openStream,
OptionalLong contentLength,
String contentType
) {}
For file-backed content:
record FileBody(Path path, long size, String contentType) {}
Do not pretend metadata does not exist. Model it explicitly.
20. Designing a Body Abstraction
For complex systems, a small source abstraction can be better than raw Java types.
interface BodySource {
InputStream openStream() throws IOException;
OptionalLong contentLength();
boolean replayable();
}
File implementation:
record PathBodySource(Path path) implements BodySource {
@Override
public InputStream openStream() throws IOException {
return Files.newInputStream(path);
}
@Override
public OptionalLong contentLength() {
try {
return OptionalLong.of(Files.size(path));
} catch (IOException e) {
return OptionalLong.empty();
}
}
@Override
public boolean replayable() {
return true;
}
}
One-shot implementation:
final class OneShotBodySource implements BodySource {
private InputStream input;
OneShotBodySource(InputStream input) {
this.input = Objects.requireNonNull(input);
}
@Override
public synchronized InputStream openStream() throws IOException {
if (input == null) {
throw new IOException("body source already consumed");
}
InputStream result = input;
input = null;
return result;
}
@Override
public OptionalLong contentLength() {
return OptionalLong.empty();
}
@Override
public boolean replayable() {
return false;
}
}
This is more verbose, but it makes retry semantics explicit.
21. Designing a Sink Abstraction
Similarly, a sink may need metadata and commit behavior.
interface BodySink {
OutputStream openStream() throws IOException;
void commit() throws IOException;
void abort() throws IOException;
}
This is useful when output should be staged before becoming visible.
Example contract:
openStream -> write bytes -> close stream -> commit
failure before commit -> abort
This is better than handing out a raw OutputStream when atomic visibility matters.
22. Naming Matters
IO API names should reveal lifecycle and materialization.
| Name | Expected Meaning |
|---|---|
readAll | may materialize everything |
stream | incremental/lazy, likely requires close |
copy | transfers bytes, may not close |
writeTo | writes to caller-owned sink |
open | returns resource caller must close |
with | callback owns temporary resource scope |
parse | consumes input, produces structured result |
forEach | streaming callback, no full collection |
load | often full materialization |
save | may own destination lifecycle |
Bad naming creates wrong mental models.
Example:
byte[] streamFile(Path path)
This name is contradictory.
23. Exception Design
IO APIs should not erase IOException unless there is a clear abstraction reason.
Good low-level API:
void writeTo(OutputStream out) throws IOException;
Domain-level API may wrap:
try {
repository.store(body);
} catch (IOException e) {
throw new DocumentStorageException("failed to store document " + id, e);
}
Rules:
- Preserve cause.
- Add boundary context.
- Do not swallow cleanup failure silently if it matters.
- Avoid vague unchecked wrappers at low levels.
- Separate validation failure from transport/storage failure.
Example:
throw new IOException("failed while writing invoice export to " + target, e);
This is much better than:
throw new IOException("failed");
24. Blocking Semantics in API Design
Make blocking behavior visible.
Bad:
void send(Event event);
Does this block on network IO? Disk? Queue?
Better:
void writeEvent(Event event) throws IOException;
or:
CompletionStage<Void> writeEventAsync(Event event);
If an API is blocking, make it acceptable by contract.
If async, define:
- executor ownership
- cancellation semantics
- buffer ownership after call
- when resource can be closed
- how exceptions are surfaced
Async IO with unclear ownership is worse than blocking IO with clear ownership.
25. API Boundary Matrix
| Requirement | Prefer |
|---|---|
| Callee owns file lifecycle | Path |
| Caller owns opened byte source | InputStream |
| Caller owns opened text source | Reader |
| Small bounded binary payload | byte[] |
| Small bounded text payload | String |
| Random access | SeekableByteChannel / FileChannel |
| Positional file writes | FileChannel |
| Retryable streaming source | Supplier<InputStream> or custom BodySource |
| Callee owns temporary resource scope | callback / withX method |
| Large record processing | handler/visitor callback |
| IO-backed lazy record stream | Stream<T> with explicit close contract |
| ByteBuffer-native parser | ByteBuffer with position contract |
| Atomic output visibility | custom sink abstraction / Path staging |
26. Case Study: Parser API
Suppose we need to parse a custom binary document format.
Naive API:
Document parse(byte[] bytes);
This forces full materialization.
Better set:
Document parse(InputStream in) throws IOException;
Document parse(Path path) throws IOException;
Implementation:
Document parse(Path path) throws IOException {
try (InputStream in = new BufferedInputStream(Files.newInputStream(path))) {
return parse(in);
}
}
This gives callers flexibility.
If the parser needs random access:
Document parse(SeekableByteChannel channel) throws IOException;
Then the Path overload can open a FileChannel.
Document parse(Path path) throws IOException {
try (SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
return parse(channel);
}
}
Design principle:
Provide convenience overloads around one precise core abstraction.
27. Case Study: Export API
Suppose a service generates large CSV exports.
Bad:
String exportCsv(Query query);
This materializes the entire export as a string.
Better:
void writeCsv(Query query, Writer writer) throws IOException;
Convenience:
void writeCsv(Query query, Path path, Charset charset) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(path, charset)) {
writeCsv(query, writer);
}
}
For HTTP frameworks:
void writeCsv(Query query, OutputStream out) throws IOException {
Writer writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
writeCsv(query, writer);
writer.flush();
}
Do not close framework-owned out unless the framework contract says to.
28. Case Study: Upload API With Retry
Bad:
void upload(InputStream body) throws IOException;
If upload fails after partial send, retry is impossible.
Better:
void upload(BodySource body) throws IOException;
The implementation can check:
if (!body.replayable() && retryPolicy.maxAttempts() > 1) {
throw new IllegalArgumentException("retry requires replayable body");
}
This prevents impossible behavior from being discovered during an outage.
29. Case Study: Ingestion API With Atomic File Visibility
Bad:
OutputStream openFinalOutput(String objectName) throws IOException;
This exposes partial final output if the writer fails halfway.
Better:
interface StagedWrite {
OutputStream openStream() throws IOException;
void commit() throws IOException;
void abort() throws IOException;
}
Usage:
StagedWrite write = store.stage(objectName);
boolean committed = false;
try (OutputStream out = write.openStream()) {
source.transferTo(out);
committed = true;
}
if (committed) {
write.commit();
} else {
write.abort();
}
Production version should handle exceptions more carefully, but the state model is clear.
30. Overload Strategy
Good IO APIs often provide layered overloads.
Example:
public Document parse(Path path) throws IOException;
public Document parse(InputStream input) throws IOException;
private Document parse(DocumentInput input) throws IOException;
But too many overloads can confuse ownership.
Guidelines:
- Choose one core abstraction.
- Add convenience overloads only when they clarify lifecycle.
- Keep close behavior consistent.
- Avoid overloads that differ only subtly in ownership.
- Document whether streams are closed.
Bad overload pair:
parse(InputStream in); // does not close
parse(FileInputStream in); // closes
This is surprising.
31. Resource Ownership Documentation Template
Use this template in JavaDoc for IO APIs:
/**
* Writes the encoded document to {@code output}.
*
* <p>Resource ownership: this method does not close {@code output}.
* It may call {@link OutputStream#flush()} after writing a complete document.
*
* <p>Partial output: if an exception is thrown, {@code output} may contain
* a partial document.
*
* <p>Threading: this method is blocking and must not be called on an event-loop thread.
*
* @throws IOException if reading, encoding, or writing fails
*/
void writeTo(OutputStream output) throws IOException;
This is not bureaucracy. It prevents production ambiguity.
32. Anti-Patterns
32.1 Ambiguous Close Behavior
void process(InputStream in)
No one knows who closes it.
32.2 Full Materialization by Default
byte[] generateHugeReport()
32.3 Charset Hidden Inside Method
Config parse(InputStream in) // silently uses default charset
32.4 One-Shot Source With Retry Policy
upload(InputStream in, RetryPolicy retry)
Retry may be impossible.
32.5 Mutable ByteBuffer Retained Without Ownership
void enqueue(ByteBuffer buffer)
Can caller mutate after enqueue?
32.6 Java Stream Returned Without Close Contract
Stream<Record> records()
May leak an open file.
32.7 Path API That Only Needs Bytes
parse(Path path)
If the parser does not need filesystem semantics, also provide InputStream.
33. Design Checklist
For each IO API, specify:
- Is the data binary or text?
- Is size bounded?
- Is payload materialized or streamed?
- Who opens the resource?
- Who closes the resource?
- Is data one-shot or replayable?
- Can the method block?
- Can it be cancelled?
- Is timeout supported?
- Is output partial on failure?
- Does the method flush?
- Does the method call
forceor provide durability? - Is charset explicit?
- Does API need metadata?
- Does API need random access?
- Does API mutate
ByteBufferposition? - Can the object escape the method scope?
- What exceptions are thrown?
- What is safe to retry?
- What is the maximum memory usage?
34. Practice Lab: Redesign Bad IO APIs
Redesign these APIs.
34.1 Bad API A
byte[] download(String id);
Questions:
- Is the object always small?
- Should caller stream to file?
- Does API need metadata?
- Can download be resumed?
Potential redesign:
void downloadTo(String id, OutputStream sink) throws IOException;
BodySource openDownload(String id) throws IOException;
34.2 Bad API B
void importUsers(String csv);
Potential redesign:
ImportResult importUsers(Reader reader) throws IOException;
ImportResult importUsers(Path path, Charset charset) throws IOException;
34.3 Bad API C
void upload(InputStream input, boolean retry);
Potential redesign:
void upload(BodySource source, RetryPolicy retryPolicy) throws IOException;
34.4 Bad API D
Stream<String> readLines(Path path);
Potential redesign:
void forEachLine(Path path, Charset charset, IOConsumer<String> consumer) throws IOException;
or document close clearly:
Stream<String> openLines(Path path, Charset charset) throws IOException;
35. Mental Model Summary
Choosing an IO API type is choosing a boundary contract.
Use:
Pathwhen file identity/lifecycle matters.InputStreamfor one-shot sequential bytes.OutputStreamfor caller-owned sequential byte sinks.Reader/Writerafter charset boundary.byte[]/Stringfor small bounded materialized payloads.ByteBufferwhen position/limit/channel interop matters.Channelwhen NIO semantics matter.Supplier<InputStream>or custom source when replayability matters.- callbacks when callee should own resource scope.
- custom source/sink abstractions when metadata, commit, retry, or lifecycle is part of the domain.
The strongest API designs make impossible states hard to express.
A weak API says:
void process(InputStream input);
A strong API says:
void processReplayable(BodySource source, ProcessingPolicy policy) throws IOException;
or:
void process(Path path, Charset charset) throws IOException;
depending on what the operation truly needs.
Good IO API design is not about using the fanciest abstraction.
It is about making ownership, lifetime, size, retry, blocking, encoding, and partial failure explicit.
36. References
- Java SE 25 API —
java.iopackage: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/io/package-summary.html - Java SE 25 API —
InputStream: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/io/InputStream.html - Java SE 25 API —
OutputStream: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/io/OutputStream.html - Java SE 25 API —
Reader: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/io/Reader.html - Java SE 25 API —
Writer: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/io/Writer.html - Java SE 25 API —
Path: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/nio/file/Path.html - Java SE 25 API —
Files: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/nio/file/Files.html - Java SE 25 API —
ByteBuffer: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/nio/ByteBuffer.html - Java SE 25 API —
java.nio.channelspackage: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/nio/channels/package-summary.html
You just completed lesson 22 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.