skip to content

How does connection pooling work in Spring LDAP, and why prefer PooledContextSource (pool2) over built-in JNDI pooling?

level: seniorimportance: nice to knowfreq 12%

answer

  1. binds are expensive -> pool connections
  2. JNDI native: setPooled(true), no validation
  3. pool2 PooledContextSource wraps LdapContextSource + testOnBorrow
  4. DefaultDirContextValidator evicts stale conns
  5. gotcha: underlying pooled=false to avoid double-pooling

basics

~10 s

Opening an LDAP connection per request is expensive, so you pool them. LDAP supports JNDI's built-in pooling (pooled=true) but it can't validate connections. Spring's PooledContextSource (commons-pool2) wraps your LdapContextSource and can test/evict stale connections.

solid answer

~40 s

LDAP binds are costly, so connections should be pooled. Two options: (1) JNDI-native pooling — set `LdapContextSource.setPooled(true)`, which flips the JNDI `com.sun.jndi.ldap.connect.pool` property. It's simple but has no connection *validation* or eviction, so connections idle-killed by a firewall or the server surface as errors on next use. (2) Spring's own pool built on **commons-pool2**: `org.springframework.ldap.pool2.factory.PooledContextSource` wraps a target `LdapContextSource` and adds `testOnBorrow`/`testWhileIdle` validation via a `DirContextValidator` (`DefaultDirContextValidator`), min/max idle, max total, and eviction of dead connections. This is the recommended approach for robustness. Critical gotcha: when using PooledContextSource you must set the *underlying* context source's `pooled` to false, otherwise you double-pool. Validation catches stale connections behind NAT/firewall idle timeouts and re-authenticated binds.

code

java · 33 lines
java
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.ldap.pool2.factory.PoolConfig;
import org.springframework.ldap.pool2.factory.PooledContextSource;
import org.springframework.ldap.pool2.validation.DefaultDirContextValidator;

@Bean
LdapContextSource targetContextSource() {
    LdapContextSource cs = new LdapContextSource();
    cs.setUrl("ldap://directory.example.com:389");
    cs.setBase("dc=example,dc=com");
    cs.setUserDn("cn=svc-account,dc=example,dc=com");
    cs.setPassword("secret");
    cs.setPooled(false); // IMPORTANT: let pool2 pool, not JNDI
    return cs;
}

@Bean
PooledContextSource pooledContextSource(LdapContextSource target) {
    PoolConfig pool = new PoolConfig();
    pool.setTestOnBorrow(true);
    pool.setTestWhileIdle(true);
    pool.setMaxTotal(20);
    pool.setMinIdle(2);
    PooledContextSource pcs = new PooledContextSource(pool);
    pcs.setContextSource(target);
    pcs.setDirContextValidator(new DefaultDirContextValidator());
    return pcs;
}

@Bean
LdapTemplate ldapTemplate(PooledContextSource pcs) {
    return new LdapTemplate(pcs);
}

go deeper

for a junior

Know that LDAP connections are pooled because binds are expensive.

for a middle

Enable native pooling via setPooled(true) and know it lacks validation.

for a senior

Configure PooledContextSource with a DirContextValidator, size the pool, and avoid double-pooling with pooled=false.

for a principal

Reason about idle-timeout topologies (firewalls/LBs), TLS interactions, per-user vs service-bind pooling, and set the org's context-source strategy.

**Why pool at all.** Each new LDAP connection involves a TCP connect plus an LDAP **bind** (authentication handshake), which is comparatively expensive. Under load, creating and tearing down a connection per operation destroys throughput. Pooling keeps a set of authenticated `DirContext` connections warm and hands them out. **Option 1 — JNDI-native pooling.** JNDI itself can pool LDAP connections through the environment property `com.sun.jndi.ldap.connect.pool=true`. Spring exposes this via `LdapContextSource.setPooled(true)`. It is zero-extra-dependency and simple. **Limitations:** it is tuned only through JVM-wide system properties (`com.sun.jndi.ldap.connect.pool.maxsize`, `.prefsize`, `.timeout`, etc.), pools *only anonymous or simple-bind* connections matching the same identity, and crucially performs **no health validation** — a connection silently dropped by an idle-timeout firewall, load balancer, or server stays in the pool and fails on next borrow. It also can't distinguish a broken context from a good one before use. **Option 2 — Spring's commons-pool2 pooling (recommended).** Spring LDAP ships a dedicated pool built on Apache **commons-pool2**. The class is **`org.springframework.ldap.pool2.factory.PooledContextSource`** (there was an older commons-pool v1 variant `org.springframework.ldap.pool.factory.PoolingContextSource`). You give it a `PoolConfig` and a **target** `LdapContextSource`; it borrows/returns pooled `DirContext`s and adds: - **Validation** — `testOnBorrow`, `testOnReturn`, `testWhileIdle` using a `DirContextValidator` (default `org.springframework.ldap.pool2.validation.DefaultDirContextValidator`, which does a cheap search against a base/filter). Invalid connections are evicted and replaced — this is the whole point: it survives firewall/idle-timeout drops. - **Sizing** — `maxTotal`, `maxIdlePerKey`/`minIdlePerKey`, `maxTotalPerKey`, and separate read-only vs read-write pools. - **Eviction** — background evictor via `timeBetweenEvictionRunsMillis`, `minEvictableIdleTimeMillis`. **The critical wiring gotcha.** When you wrap a context source in `PooledContextSource`, the **underlying** `LdapContextSource` must have `setPooled(false)` (the default). If you leave native JNDI pooling on *and* wrap it in the commons-pool2 pool, you get two pooling layers fighting each other — connections that pool2 thinks it owns are also being pooled/closed by JNDI, producing subtle corruption and leaks. Rule: pick exactly one pooling mechanism. **Other gotchas.** - Pooling caches the *bind identity*. If each request authenticates as a *different* user (e.g. per-user binds), a single shared pooled context source is wrong; use a fresh authentication instead, or Spring Security's LDAP auth. Pooling is ideal for a single service/bind account doing lookups. - Validation costs a round-trip; set a cheap base DN and filter for `DefaultDirContextValidator`. - With TLS/`StartTLS`, native JNDI pooling interacts badly; commons-pool2 with proper validation is safer. - Spring Boot auto-config wires a plain `LdapContextSource` and `LdapTemplate`; enabling pool2 pooling means declaring `PooledContextSource` beans yourself (add the `spring-ldap-core` pool2 support and commons-pool2 dependency). **When to use which.** For anything production-facing behind firewalls/load balancers with long-lived idle periods, use **PooledContextSource (pool2) with validation**. For a quick internal tool with a chatty, always-active connection, native `pooled=true` may suffice.

  • Why is validation (testOnBorrow) the main reason to choose pool2 over native JNDI pooling?
    Firewalls, load balancers, and servers silently drop idle LDAP connections; native JNDI pooling has no health check and hands out a dead connection that fails on use, while pool2's DirContextValidator tests and evicts stale connections before borrowing.
  • What breaks if you enable both setPooled(true) and PooledContextSource?
    You double-pool: JNDI and commons-pool2 both manage the same connections, causing leaks and corruption. The underlying context source must have pooled=false.
  • When is pooling with a single context source the wrong choice?
    When each request binds as a different user (per-user authentication); a pool caches one bind identity, so you'd reuse the wrong credentials. Use fresh binds or Spring Security LDAP auth instead.

saying these in an interview costs you the question

  • Believing JNDI-native pooling validates/health-checks connections (it does not)
  • Enabling both native pooling and PooledContextSource simultaneously
  • Pooling a shared context source while doing per-user authentication binds
  • Assuming Spring Boot auto-config enables commons-pool2 pooling by default (it wires a plain LdapContextSource)

context