Loading content...
Resource acquisition often involves more than simple construction: validation, configuration, connection establishment, and cleanup registration. Factory methods encapsulate this complexity behind clean interfaces, ensuring resources are correctly initialized and ready for scope-based management.
This page explores factory patterns that hide acquisition complexity while producing resources that integrate seamlessly with try-with-resources, using statements, and RAII patterns.
By the end of this page, you will master factory patterns for resources, understand when to use static factories vs. builders vs. pooling, learn to design factories that produce RAII-compliant resources, and see how factories integrate with dependency injection.
Direct construction of resources exposes too many details to callers. Factory methods provide a layer of abstraction that offers significant benefits:
123456789101112131415161718192021222324252627282930313233343536373839
// Direct construction - caller must know all the detailspublic void directConstruction() throws SQLException { Connection conn = DriverManager.getConnection( "jdbc:postgresql://localhost:5432/mydb", "user", "password" ); conn.setAutoCommit(false); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); conn.setNetworkTimeout(executor, 30000); // Now use conn...} // Factory method - all complexity hiddenpublic void factoryConstruction() throws SQLException { try (Connection conn = connectionFactory.createConnection()) { // Already configured, ready to use conn.prepareStatement("SELECT 1").execute(); }} // The factory encapsulates all configurationpublic class ConnectionFactory { private final String url; private final Properties config; public ConnectionFactory(DatabaseConfig config) { this.url = config.buildJdbcUrl(); this.config = config.toProperties(); } public Connection createConnection() throws SQLException { Connection conn = DriverManager.getConnection(url, config); conn.setAutoCommit(false); conn.setTransactionIsolation( Connection.TRANSACTION_READ_COMMITTED); return conn; }}Static factory methods are class-level methods that create and return instances. They offer naming flexibility, can return subtypes, and cache instances when appropriate.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
public class FileHandle implements AutoCloseable { private final RandomAccessFile file; private final Path path; // Private constructor - force use of factories private FileHandle(Path path, RandomAccessFile file) { this.path = path; this.file = file; } // Named factory methods clarify intent public static FileHandle forReading(Path path) throws IOException { validatePath(path); return new FileHandle(path, new RandomAccessFile(path.toFile(), "r")); } public static FileHandle forWriting(Path path) throws IOException { validatePath(path); ensureParentExists(path); return new FileHandle(path, new RandomAccessFile(path.toFile(), "rw")); } public static FileHandle forAppending(Path path) throws IOException { FileHandle handle = forWriting(path); handle.file.seek(handle.file.length()); return handle; } // Factory returning Optional for cases that might fail public static Optional<FileHandle> tryOpen(Path path) { try { return Optional.of(forReading(path)); } catch (IOException e) { return Optional.empty(); } } @Override public void close() throws IOException { file.close(); } private static void validatePath(Path path) { Objects.requireNonNull(path, "Path cannot be null"); } private static void ensureParentExists(Path path) throws IOException { Files.createDirectories(path.getParent()); }} // Usagetry (var file = FileHandle.forWriting(Paths.get("output.txt"))) { file.write(data);}When resources require many configuration options, the Builder pattern provides a fluent interface for configuration before acquisition. The build() method performs all validation and returns a fully configured, ready-to-use resource.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
public class HttpConnection implements AutoCloseable { private final HttpURLConnection connection; private HttpConnection(HttpURLConnection connection) { this.connection = connection; } public static Builder builder(String url) { return new Builder(url); } public static class Builder { private final String url; private String method = "GET"; private int timeout = 30000; private Map<String, String> headers = new HashMap<>(); private boolean followRedirects = true; private Builder(String url) { this.url = Objects.requireNonNull(url); } public Builder method(String method) { this.method = method; return this; } public Builder timeout(int millis) { this.timeout = millis; return this; } public Builder header(String key, String value) { this.headers.put(key, value); return this; } public Builder followRedirects(boolean follow) { this.followRedirects = follow; return this; } // Build acquires the resource with all configuration public HttpConnection build() throws IOException { URL parsedUrl = new URL(url); HttpURLConnection conn = (HttpURLConnection) parsedUrl.openConnection(); conn.setRequestMethod(method); conn.setConnectTimeout(timeout); conn.setReadTimeout(timeout); conn.setInstanceFollowRedirects(followRedirects); for (var entry : headers.entrySet()) { conn.setRequestProperty(entry.getKey(), entry.getValue()); } return new HttpConnection(conn); } } @Override public void close() { connection.disconnect(); }} // Usagetry (var conn = HttpConnection.builder("https://api.example.com/data") .method("POST") .timeout(5000) .header("Authorization", "Bearer " + token) .header("Content-Type", "application/json") .build()) { // Use connection}For expensive-to-create resources like database connections, pool-based factories return resources from a pre-allocated pool. The factory ensures callers receive properly configured resources that return to the pool on close.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
public class ConnectionPool implements AutoCloseable { private final BlockingQueue<Connection> available; private final Set<Connection> inUse; private final ConnectionFactory factory; private volatile boolean closed = false; public ConnectionPool(ConnectionFactory factory, int size) { this.factory = factory; this.available = new LinkedBlockingQueue<>(size); this.inUse = ConcurrentHashMap.newKeySet(); // Pre-populate pool for (int i = 0; i < size; i++) { available.offer(factory.create()); } } // Factory method returns pooled wrapper public PooledConnection acquire() throws InterruptedException { if (closed) throw new IllegalStateException("Pool closed"); Connection conn = available.poll(30, TimeUnit.SECONDS); if (conn == null) { throw new TimeoutException("No connections available"); } inUse.add(conn); return new PooledConnection(conn, this::release); } private void release(Connection conn) { if (inUse.remove(conn) && !closed) { available.offer(conn); } } @Override public void close() { closed = true; // Close all connections for (Connection c : available) { c.close(); } for (Connection c : inUse) { c.close(); } }} // Wrapper that returns to pool on closepublic class PooledConnection implements AutoCloseable { private final Connection delegate; private final Consumer<Connection> releaser; PooledConnection(Connection delegate, Consumer<Connection> releaser) { this.delegate = delegate; this.releaser = releaser; } public void execute(String sql) throws SQLException { delegate.createStatement().execute(sql); } @Override public void close() { releaser.accept(delegate); // Return to pool, not close }} // Usage - transparent poolingtry (var conn = pool.acquire()) { conn.execute("SELECT 1");} // Connection returned to poolIn dependency injection containers, Provider or Factory interfaces create resources on demand while benefiting from container-managed configuration and lifecycle.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
@Configurationpublic class ResourceConfig { @Bean public ConnectionFactory connectionFactory(DatabaseProperties props) { return new ConnectionFactory(props); } // Prototype scope = new instance per injection @Bean @Scope("prototype") public FileProcessor fileProcessor( ConnectionFactory connFactory, @Value("${processing.timeout}") int timeout) { return new FileProcessor(connFactory, timeout); }} // Provider pattern for on-demand creation@Servicepublic class ReportGenerator { private final Provider<DatabaseSession> sessionProvider; public ReportGenerator(Provider<DatabaseSession> sessionProvider) { this.sessionProvider = sessionProvider; } public Report generate() { // Each call gets a fresh session try (DatabaseSession session = sessionProvider.get()) { return session.query("SELECT * FROM reports"); } }} // Factory interface for parameterized creationpublic interface ReportSessionFactory { DatabaseSession create(ReportType type);} @Componentpublic class ReportSessionFactoryImpl implements ReportSessionFactory { private final ConnectionPool pool; private final Map<ReportType, Config> configs; @Override public DatabaseSession create(ReportType type) { Config config = configs.get(type); return new DatabaseSession(pool.acquire(), config); }}Factories must handle failures gracefully, cleaning up partially-acquired resources and providing meaningful error information.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
public class DatabaseSession implements AutoCloseable { private Connection connection; private PreparedStatement healthCheck; public static DatabaseSession create(DataSource ds) throws SQLException { Connection conn = null; PreparedStatement stmt = null; try { conn = ds.getConnection(); conn.setAutoCommit(false); // Validate connection stmt = conn.prepareStatement("SELECT 1"); stmt.execute(); return new DatabaseSession(conn, stmt); } catch (SQLException e) { // Clean up partially acquired resources if (stmt != null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } throw new DatabaseConnectionException("Failed to create session", e); } } private DatabaseSession(Connection conn, PreparedStatement healthCheck) { this.connection = conn; this.healthCheck = healthCheck; } @Override public void close() throws SQLException { SQLException first = null; try { healthCheck.close(); } catch (SQLException e) { first = e; } try { connection.close(); } catch (SQLException e) { if (first != null) first.addSuppressed(e); else first = e; } if (first != null) throw first; }}If a factory acquires resource A, then fails acquiring resource B, it must clean up A before throwing. Use try/catch blocks in factories to ensure no resources leak on partial failure. This is especially important when acquiring multiple related resources.
You've mastered Resource Acquisition Patterns: RAII binds resources to objects, try-with-resources provides language-level cleanup, scope-based management makes safety structural, and factories encapsulate acquisition complexity. Apply these patterns consistently to eliminate resource leaks from your systems.