Best practices
Follow these best practices to get the most out of the Developer SDK in production environments.
Client sharing patterns
Share the cluster instance
Create one Cluster instance per application and share it. The Cluster manages connection pooling, node discovery, and cluster state.
import com.aerospike.client.sdk.policy.Behavior;import com.aerospike.client.sdk.Cluster;import com.aerospike.client.sdk.ClusterDefinition;import com.aerospike.client.sdk.Host;import com.aerospike.client.sdk.Session;
// ✅ Good: Single cluster instance, shared across your apppublic class AerospikeConfig { private static final Cluster cluster = new ClusterDefinition( new Host("aerospike-1.example.com", 3000), new Host("aerospike-2.example.com", 3000) ).connect();
public static Cluster getCluster() { return cluster; }}
// Usage in servicespublic class UserService { private final Session session;
public UserService() { this.session = AerospikeConfig.getCluster() .createSession(Behavior.DEFAULT); }}from aerospike_sdk import Behavior, ClusterDefinition, Host
# ✅ Good: Single cluster instance, shared across your appclass AerospikeConfig: _cluster = None
@classmethod async def get_cluster(cls): if cls._cluster is None: cls._cluster = await ClusterDefinition( hosts=[ Host.of("aerospike-1.example.com", 3000), Host.of("aerospike-2.example.com", 3000), ] ).connect() return cls._cluster
# Usage in services (call await AerospikeConfig.get_cluster() once at app startup,# then inject the shared cluster into services)class UserService: def __init__(self, cluster): self.session = cluster.create_session(Behavior.DEFAULT)Don’t create clusters per request
import com.aerospike.client.sdk.Cluster;import com.aerospike.client.sdk.ClusterDefinition;import com.aerospike.client.sdk.DataSet;import com.aerospike.client.sdk.Record;import com.aerospike.client.sdk.RecordStream;import com.aerospike.client.sdk.Session;
// ❌ Bad: Creating a new cluster per request wastes resourcespublic Record getUser(String userId) { DataSet users = DataSet.of("test", "users"); try (Cluster cluster = new ClusterDefinition("localhost", 3000).connect()) { // Don't do this per request! Session session = cluster.createSession(Behavior.DEFAULT); Record out; try (RecordStream stream = session.query(users.id(userId)).execute()) { out = stream.getFirstRecord(); } return out; }}from aerospike_sdk import ClusterDefinition, DataSet
# ❌ Bad: Creating a new cluster per request wastes resourcesasync def get_user(user_id): users = DataSet.of("test", "users") async with await ClusterDefinition("localhost", 3000).connect() as cluster: # Don't do this! session = cluster.create_session(Behavior.DEFAULT) stream = await session.query(users.id(user_id)).execute() row = await stream.first_or_raise() record = row.record_or_raise() stream.close() return recordSession management
Use multiple sessions for different workloads
Create dedicated sessions for different operation types:
public class DatabaseService { private final Session readSession; private final Session writeSession; private final Session criticalSession;
public DatabaseService(Cluster cluster) { Behavior readFast = Behavior.DEFAULT.deriveWithChanges("READ_FAST", b -> {}); Behavior criticalWrites = Behavior.DEFAULT.deriveWithChanges("CRITICAL_WRITES", b -> {});
// Fast reads for user-facing queries this.readSession = cluster.createSession(readFast);
// Standard writes this.writeSession = cluster.createSession(Behavior.DEFAULT);
// Critical operations (payments, audit logs) this.criticalSession = cluster.createSession(criticalWrites); }}class DatabaseService: def __init__(self, cluster): # Fast reads for user-facing queries self.read_session = cluster.create_session(Behavior.READ_FAST)
# Standard writes self.write_session = cluster.create_session(Behavior.DEFAULT)
# Critical operations (payments, audit logs) self.critical_session = cluster.create_session(Behavior.DEFAULT)Resource cleanup
Always close resources
Close sessions and clusters when your application shuts down:
import com.aerospike.client.sdk.policy.Behavior;import com.aerospike.client.sdk.Cluster;import com.aerospike.client.sdk.ClusterDefinition;import com.aerospike.client.sdk.DataSet;import com.aerospike.client.sdk.Session;
// Using try-with-resourcesDataSet users = DataSet.of("test", "users");try (Cluster cluster = new ClusterDefinition("localhost", 3000).connect()) { Session session = cluster.createSession(Behavior.DEFAULT); session.insert(users) .bins("name") .id("user-1").values("Alice") .execute();}
// Or with shutdown hooksRuntime.getRuntime().addShutdownHook(new Thread(() -> { session.close(); cluster.close();}));from aerospike_sdk import Behavior, ClusterDefinition, DataSet
# Using async context managers (typical async app)users = DataSet.of("test", "users")async with await ClusterDefinition("localhost", 3000).connect() as cluster: session = cluster.create_session(Behavior.DEFAULT) await session.insert(key=users.id("user-1")).put({"name": "Alice"}).execute()
# Or register cleanup for a long-lived cluster from app startupimport atexit
# cluster = ... # created once at startup (see Connect guide)# session = cluster.create_session(Behavior.DEFAULT)# atexit.register(session.close)# atexit.register(cluster.close)Connection limits
Size your connection pool appropriately
The default connection pool is usually sufficient, but high-throughput applications may need tuning:
Cluster cluster = new ClusterDefinition("localhost", 3000) .withSystemSettings(builder -> builder .connections(ops -> ops.maximumConnectionsPerNode(300)) // Default is usually 100 ) .connect();from aerospike_sdk import ClusterDefinitionfrom aerospike_sdk.policy.system_settings import SystemSettings
cluster = await ( ClusterDefinition("localhost", 3000) .with_system_settings( SystemSettings(max_connections_per_node=300) # Default is usually 100 ) .connect())Behavior reuse
Define behaviors once, reuse everywhere
// ✅ Good: Define behaviors as constantspublic class Behaviors { public static final Behavior FAST_READ = Behavior.DEFAULT.deriveWithChanges("FAST_READ", b -> {});
public static final Behavior SAFE_WRITE = Behavior.DEFAULT.deriveWithChanges("SAFE_WRITE", b -> {});}
// UsageSession fastSession = cluster.createSession(Behaviors.FAST_READ);Session safeSession = cluster.createSession(Behaviors.SAFE_WRITE);# ✅ Good: Define behaviors as module-level constantsFAST_READ = Behavior.READ_FASTSAFE_WRITE = Behavior.DEFAULT
# Usagefast_session = cluster.create_session(FAST_READ)safe_session = cluster.create_session(SAFE_WRITE)Error handling
Handle errors at the right level
import com.aerospike.client.sdk.DataSet;import com.aerospike.client.sdk.AerospikeException.RecordExistsException;import com.aerospike.client.sdk.AerospikeException.Timeout;import com.aerospike.client.sdk.AerospikeException;
// ✅ Good: Catch specific exceptions, handle appropriatelyDataSet users = DataSet.of("test", "users");try { session.insert(users) .bins("name") .id(userId).values(name) .execute();} catch (RecordExistsException e) { // Expected case: user already exists log.info("User {} already exists, skipping", userId);} catch (Timeout e) { // Transient: might succeed on retry throw new RetryableException("Database timeout", e);} catch (AerospikeException e) { // Unexpected: log and fail log.error("Unexpected database error", e); throw new DatabaseException("Failed to create user", e);}from aerospike_sdk import AerospikeError, ConnectionError, DataSet, TimeoutErrorfrom aerospike_async.exceptions import ResultCode
# ✅ Good: Catch specific exceptions, handle appropriatelyusers = DataSet.of("test", "users")try: await session.insert(key=users.id(user_id)).put({"name": name}).execute()except TimeoutError as e: # Transient: might succeed on retry raise RetryableException("Database timeout") from eexcept ConnectionError as e: raise RetryableException("Database connection failed") from eexcept AerospikeError as e: if e.result_code == ResultCode.KEY_EXISTS_ERROR: logger.info(f"User {user_id} already exists, skipping") else: logger.error("Unexpected database error", exc_info=True) raise DatabaseException("Failed to create user") from eSee Handle Errors Gracefully for detailed error handling patterns.
Production recommendations
Pre-flight checklist
Before deploying to production:
- Connection pooling: Verify pool size matches your concurrency needs
- Timeouts: Set appropriate timeouts for your SLAs
- Retries: Configure retry behavior for transient failures
- Monitoring: Enable metrics collection
- Logging: Configure appropriate log levels
- Resource cleanup: Ensure proper shutdown handling
Monitoring essentials
Track these metrics in production:
| Metric | Why It Matters |
|---|---|
| Operation latency (p50, p95, p99) | Detect performance degradation |
| Error rate by type | Identify systematic issues |
| Connection pool utilization | Prevent connection exhaustion |
| Retry rate | Detect cluster instability |
Next steps
Tune Performance & Reliability
Configure Behaviors for your workload.
Handle Errors Gracefully
Implement robust error handling.
Configuration Options
Externalize and manage configuration.