|
| 1 | +# Guide: AWS Aurora with Dual DataSources (Read-Write and Read-Only) |
| 2 | + |
| 3 | +## Purpose |
| 4 | + |
| 5 | +This guide provides step-by-step instructions for configuring ebean-datasource with AWS Aurora using two separate DataSource pools: one for read-write operations (primary endpoint) and one for read-only operations (reader endpoint). This pattern is commonly used to offload read traffic from the primary database. |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## Prerequisites |
| 10 | + |
| 11 | +- AWS Aurora database cluster (MySQL or PostgreSQL compatible) |
| 12 | +- Two Aurora endpoints available: |
| 13 | + - **Write endpoint:** For INSERT, UPDATE, DELETE, SELECT and transaction operations |
| 14 | + - **Reader endpoint:** For SELECT queries (read replica endpoint) |
| 15 | +- A Java project with Maven |
| 16 | +- `io.ebean:ebean-datasource` dependency already added |
| 17 | +- Ebean ORM configured for secondary datasource support |
| 18 | +- Database user credentials for both endpoints (can be same user if both endpoints support it) |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +## What is the Aurora Reader Endpoint? |
| 23 | + |
| 24 | +AWS Aurora provides two types of endpoints: |
| 25 | + |
| 26 | +- **Write endpoint (Cluster endpoint):** `mydb-cluster.123456789.us-east-1.rds.amazonaws.com` |
| 27 | + - Routes all write operations to the primary database instance |
| 28 | + - Can handle both reads and writes |
| 29 | + |
| 30 | +- **Reader endpoint (Read-only endpoint):** `mydb-cluster-ro.123456789.us-east-1.rds.amazonaws.com` |
| 31 | + - Automatically distributes read operations across all read replicas |
| 32 | + - Cannot handle write operations |
| 33 | + - Better performance for read-heavy workloads |
| 34 | + |
| 35 | +--- |
| 36 | + |
| 37 | +## Scenario: Setup Dual DataSources for Aurora |
| 38 | + |
| 39 | +### Step 1 — Create the read-write (primary) DataSource |
| 40 | + |
| 41 | +This pool connects to Aurora's write endpoint and handles all write operations. |
| 42 | + |
| 43 | +```java |
| 44 | +DataSourcePool writePool = DataSourcePool.builder() |
| 45 | + .name("datasource-write") |
| 46 | + .url("jdbc:mysql://mydb-cluster.123456789.us-east-1.rds.amazonaws.com:3306/myapp") |
| 47 | + .username(System.getenv("DB_USERNAME")) |
| 48 | + .password(System.getenv("DB_PASSWORD")) |
| 49 | + .minConnections(3) |
| 50 | + .initialConnections(5) |
| 51 | + .maxConnections(20) |
| 52 | + .build(); |
| 53 | +``` |
| 54 | + |
| 55 | +### Step 2 — Create the read-only DataSource |
| 56 | + |
| 57 | +This pool connects to Aurora's reader endpoint and optimizes for read-only queries. |
| 58 | + |
| 59 | +```java |
| 60 | +DataSourcePool readOnlyPool = DataSourcePool.builder() |
| 61 | + .name("datasource-read") |
| 62 | + .url("jdbc:mysql://mydb-cluster-ro.123456789.us-east-1.rds.amazonaws.com:3306/myapp") |
| 63 | + .username(System.getenv("DB_USERNAME")) |
| 64 | + .password(System.getenv("DB_PASSWORD")) |
| 65 | + .readOnly(true) // Signal read-only mode |
| 66 | + .autoCommit(true) // Skip transaction overhead |
| 67 | + .minConnections(5) |
| 68 | + .initialConnections(10) |
| 69 | + .maxConnections(50) |
| 70 | + .build(); |
| 71 | +``` |
| 72 | + |
| 73 | +### Step 3 — Configure Ebean with dual datasources |
| 74 | + |
| 75 | +When using Ebean ORM, configure the secondary (read-only) datasource. Ebean will automatically route read queries to the secondary datasource. |
| 76 | + |
| 77 | +```java |
| 78 | +Database database = DatabaseBuilder.create() |
| 79 | + .datasource(writePool) // Primary write datasource |
| 80 | + .secondaryDatasource(readOnlyPool) // Secondary read-only datasource |
| 81 | + .name("db") |
| 82 | + .build(); |
| 83 | +``` |
| 84 | + |
| 85 | +Or with dependency injection (Avaje): |
| 86 | + |
| 87 | +```java |
| 88 | +@Factory |
| 89 | +public class DatabaseFactory { |
| 90 | + |
| 91 | + private DataSourceBuider dataSourceBuider() { |
| 92 | + var username = System.getenv("DB_USERNAME"); |
| 93 | + var password = System.getenv("DB_PASSWORD"); |
| 94 | + |
| 95 | + return DataSourcePool.builder() |
| 96 | + .username(username) |
| 97 | + .password(password); |
| 98 | + } |
| 99 | + |
| 100 | + @Bean |
| 101 | + public Database database() { |
| 102 | + |
| 103 | + var writePool = dataSourceBuider() |
| 104 | + .name("datasource-write") |
| 105 | + .url("jdbc:mysql://mydb-cluster.123456789.us-east-1.rds.amazonaws.com:3306/myapp") |
| 106 | + .minConnections(3) |
| 107 | + .initialConnections(5) |
| 108 | + .maxConnections(20) |
| 109 | + .build(); |
| 110 | + |
| 111 | + var readOnlyPool = dataSourceBuider() |
| 112 | + .name("datasource-read") |
| 113 | + .url("jdbc:mysql://mydb-cluster-ro.123456789.us-east-1.rds.amazonaws.com:3306/myapp") |
| 114 | + .readOnly(true) |
| 115 | + .autoCommit(true) |
| 116 | + .minConnections(5) |
| 117 | + .initialConnections(10) |
| 118 | + .maxConnections(50) |
| 119 | + .build(); |
| 120 | + |
| 121 | + return DatabaseBuilder.create() |
| 122 | + .name("db") |
| 123 | + .datasource(writePool) |
| 124 | + .readonlyDatasource(readOnlyPool) |
| 125 | + .build(); |
| 126 | + } |
| 127 | +} |
| 128 | +``` |
| 129 | + |
| 130 | +--- |
| 131 | + |
| 132 | +## How Ebean Routes Queries |
| 133 | + |
| 134 | +Once configured with a secondary datasource, Ebean automatically routes queries: |
| 135 | + |
| 136 | +- **Write operations** → Primary DataSource (write endpoint): |
| 137 | + - `db.insert(...)` |
| 138 | + - `db.update(...)` |
| 139 | + - `db.delete(...)` |
| 140 | + - `db.execute()` (for arbitrary SQL updates) |
| 141 | + - Transactions with writes |
| 142 | + |
| 143 | +- **Read operations** → Secondary DataSource (read endpoint): |
| 144 | + - `db.find(User.class).findList()` |
| 145 | + - `db.find(User.class).findOne()` |
| 146 | + - `db.sqlQuery("SELECT ...").findList()` |
| 147 | + - Read-only queries |
| 148 | + |
| 149 | +--- |
| 150 | + |
| 151 | +## Configuration Best Practices |
| 152 | + |
| 153 | +### Connection Sizing for Dual DataSources |
| 154 | + |
| 155 | +**Write Pool (Primary Endpoint):** |
| 156 | +```java |
| 157 | +.minConnections(3) // Fewer needed - mostly from read pool |
| 158 | +.initialConnections(5) |
| 159 | +.maxConnections(20) |
| 160 | +``` |
| 161 | + |
| 162 | +**Read Pool (Reader Endpoint):** |
| 163 | +```java |
| 164 | +.minConnections(5) // More connections for read traffic |
| 165 | +.initialConnections(10) |
| 166 | +.maxConnections(50) |
| 167 | +``` |
| 168 | + |
| 169 | +The read pool can have more connections because the reader endpoint is designed to handle distributed read load. |
| 170 | + |
| 171 | +### Using Environment Variables or external configuration |
| 172 | + |
| 173 | +Store database endpoints in environment variables or external configuration (like avaje config): |
| 174 | + |
| 175 | +```java |
| 176 | +String writeUrl = System.getenv("DB_WRITE_ENDPOINT"); |
| 177 | +String readUrl = System.getenv("DB_READ_ENDPOINT"); |
| 178 | +String user = System.getenv("DB_USERNAME"); |
| 179 | +String pass = System.getenv("DB_PASSWORD"); |
| 180 | + |
| 181 | +DataSourcePool writePool = DataSourcePool.builder() |
| 182 | + .name("datasource-write") |
| 183 | + .url(writeUrl) |
| 184 | + .username(user) |
| 185 | + .password(pass) |
| 186 | + .minConnections(3) |
| 187 | + .initialConnections(5) |
| 188 | + .maxConnections(20) |
| 189 | + .build(); |
| 190 | + |
| 191 | +DataSourcePool readOnlyPool = DataSourcePool.builder() |
| 192 | + .name("datasource-read") |
| 193 | + .url(readUrl) |
| 194 | + .username(user) |
| 195 | + .password(pass) |
| 196 | + .readOnly(true) |
| 197 | + .autoCommit(true) |
| 198 | + .minConnections(5) |
| 199 | + .initialConnections(10) |
| 200 | + .maxConnections(50) |
| 201 | + .build(); |
| 202 | +``` |
| 203 | + |
| 204 | + |
| 205 | +--- |
| 206 | + |
| 207 | +## Aurora Failover Behavior |
| 208 | + |
| 209 | +### Write Endpoint Failover |
| 210 | + |
| 211 | +If the primary instance fails, Aurora automatically promotes a read replica to be the new primary: |
| 212 | + |
| 213 | +- Aurora updates the write endpoint to point to the new primary |
| 214 | +- The reader endpoint continues to distribute reads across remaining read replicas |
| 215 | +- Your application's connection pools will eventually connect to the new primary |
| 216 | +- ebean-datasource will reset when it detects the failover |
| 217 | + |
| 218 | + |
| 219 | +### Reader Endpoint Failover |
| 220 | + |
| 221 | +If a read replica fails: |
| 222 | + |
| 223 | +- Aurora automatically removes it from the reader endpoint pool |
| 224 | +- Other read replicas continue serving read traffic |
| 225 | +- No action needed from your application |
| 226 | + |
| 227 | +--- |
| 228 | + |
| 229 | +## Monitoring and Metrics |
| 230 | + |
| 231 | +### Connection Pool Metrics |
| 232 | + |
| 233 | +Use the connection pool's built-in metrics to monitor pool health: |
| 234 | + |
| 235 | +```java |
| 236 | +ConnectionPoolStatistics stats = writePool.getStatus(); |
| 237 | +System.out.println("Write Pool - Size: " + stats.size()); |
| 238 | + |
| 239 | +ConnectionPoolStatistics readStats = readOnlyPool.getStatus(); |
| 240 | +System.out.println("Read Pool - Size: " + readStats.size()); |
| 241 | +``` |
| 242 | + |
| 243 | +### AWS CloudWatch Metrics |
| 244 | + |
| 245 | +Monitor Aurora metrics in CloudWatch: |
| 246 | +- `DatabaseConnections` - Current connections |
| 247 | +- `DatabaseCPUUtilization` - CPU usage |
| 248 | +- `ReadLatency` / `WriteLatency` - Query latency |
| 249 | +- `Connections` - Connection count per instance |
| 250 | + |
| 251 | +--- |
| 252 | + |
| 253 | +## Common Issues and Solutions |
| 254 | + |
| 255 | +### Issue: Read queries still hitting the write endpoint |
| 256 | + |
| 257 | +**Cause:** Explicit transactions or transaction context from the write pool. |
| 258 | + |
| 259 | +**Solution:** Ensure read-only queries are executed outside of transaction context: |
| 260 | + |
| 261 | +```java |
| 262 | +// This uses the read pool |
| 263 | +List<User> users = db.find(User.class).findList(); |
| 264 | + |
| 265 | +// This uses the write pool (even for reads within a transaction) |
| 266 | +try (var transaction = db.beginTransaction()) { |
| 267 | + List<User> users = db.find(User.class).findList(); // Still uses write pool |
| 268 | + // ... write operations |
| 269 | + transaction.commit(); |
| 270 | +} |
| 271 | +``` |
| 272 | + |
| 273 | +### Issue: Latency between write and read |
| 274 | + |
| 275 | +**Cause:** Aurora replication lag - read replicas may not have the latest data immediately. |
| 276 | + |
| 277 | +**Solution:** For critical reads after writes, use the write pool: |
| 278 | + |
| 279 | +```java |
| 280 | +// Write on primary |
| 281 | +db.insert(user); |
| 282 | + |
| 283 | +// Read on primary to ensure latest data |
| 284 | +User updated = db.find(User.class) |
| 285 | + .usingMaster(true) // Force primary datasource |
| 286 | + .where().idEq(user.getId()) |
| 287 | + .findOne(); |
| 288 | +``` |
| 289 | + |
| 290 | +### Issue: Too many connections on read pool |
| 291 | + |
| 292 | +**Cause:** Pool not trimming idle connections or application creating excessive load. |
| 293 | + |
| 294 | +**Solution:** Monitor connection usage and adjust `maxConnections` down: |
| 295 | + |
| 296 | +```java |
| 297 | +.maxConnections(30) // Reduce from 50 |
| 298 | +.trimPoolFreqSecs(30) // Trim more aggressively if needed |
| 299 | +``` |
| 300 | + |
| 301 | + |
| 302 | +--- |
| 303 | + |
| 304 | +## Next Steps |
| 305 | + |
| 306 | +- Read the main [README](../../README.md) for general pool configuration options |
| 307 | +- See [Creating a DataSource Pool](create-datasource-pool.md) for basic pool setup |
| 308 | +- Check the [Ebean ORM documentation](https://ebean.io) for secondary datasource configuration |
| 309 | +- Review [AWS Aurora documentation](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/) for cluster and endpoint details |
0 commit comments