<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Tipu's Tech Notes]]></title><description><![CDATA[Tipu's Tech Notes]]></description><link>https://rifat-tipu.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a38f0e29103525e06ad8540/092be6f7-d17f-410e-8a24-6e8b1bf49851.png</url><title>Tipu&apos;s Tech Notes</title><link>https://rifat-tipu.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 04 Sep 2026 09:36:40 GMT</lastBuildDate><atom:link href="https://rifat-tipu.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Race Conditions in E-Commerce: How I Discovered a Silent Bug That Could Cost Me Money]]></title><description><![CDATA[I was building a backend for a clothing store called Emras — a full-stack e-commerce platform using Spring Boot 4, PostgreSQL 18, and a modular monolith architecture. Everything was going well. Auth w]]></description><link>https://rifat-tipu.hashnode.dev/race-conditions-in-e-commerce-how-i-discovered-a-silent-bug-that-could-cost-me-money</link><guid isPermaLink="true">https://rifat-tipu.hashnode.dev/race-conditions-in-e-commerce-how-i-discovered-a-silent-bug-that-could-cost-me-money</guid><dc:creator><![CDATA[Rifat Hossain]]></dc:creator><pubDate>Wed, 12 Aug 2026 18:42:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a38f0e29103525e06ad8540/6856efe4-0330-4925-a2de-3d3b9ca2dafb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I was building a backend for a clothing store called <strong>Emras</strong> — a full-stack e-commerce platform using Spring Boot 4, PostgreSQL 18, and a modular monolith architecture. Everything was going well. Auth was working, products were loading, the cart was saving correctly.</p>
<p>Then I started thinking about promo codes.</p>
<p>Specifically, I started thinking about what happens when a promo code has only <strong>one use left</strong> and <strong>two users hit the checkout button at the exact same time</strong>.</p>
<p>That question sent me down a rabbit hole that every backend engineer needs to understand: <strong>race conditions in concurrent systems</strong>, and how to choose between <strong>optimistic locking</strong> and <strong>pessimistic locking</strong>.</p>
<hr />
<h2>What Is a Race Condition?</h2>
<p>A race condition happens when two or more processes access shared data at the same time, and the final result depends on the order in which they execute — producing incorrect behavior.</p>
<p>In our promo code scenario:</p>
<pre><code class="language-plaintext">promo_codes table:
  code = "EMRAS20"
  usage_limit = 1
  used_count  = 0
</code></pre>
<p>Without any protection, this can happen:</p>
<pre><code class="language-plaintext">Time  |  User A                          |  User B
------|----------------------------------|----------------------------------
T1    |  SELECT * FROM promo_codes       |
      |  WHERE code = 'EMRAS20'          |
      |  → used_count = 0, limit = 1 ✅  |
T2    |                                  |  SELECT * FROM promo_codes
      |                                  |  WHERE code = 'EMRAS20'
      |                                  |  → used_count = 0, limit = 1 ✅
T3    |  Places order, increments count  |
      |  UPDATE SET used_count = 1       |
T4    |                                  |  Places order, increments count
      |                                  |  UPDATE SET used_count = 2 ❌
</code></pre>
<p>Both users successfully applied a promo code that was supposed to be used only once. The business loses money. No exception was thrown. No error appeared in the logs.</p>
<p>This is the scariest kind of bug — <strong>silent data corruption</strong>.</p>
<hr />
<h2>Why This Is a Real Problem</h2>
<p>You might think: "Who would have two users hit checkout at the exact same millisecond?"</p>
<p>More people than you think.</p>
<ul>
<li><p>Flash sales where hundreds of users rush checkout simultaneously</p>
</li>
<li><p>Bots scanning for valid promo codes</p>
</li>
<li><p>Users double-clicking the checkout button</p>
</li>
<li><p>Mobile app retries on slow network</p>
</li>
</ul>
<p>At scale, even a 0.01% collision rate on a platform with 10,000 daily orders means 1 corrupted promo code per day. Over a month that's real money — and real trust — lost.</p>
<hr />
<h2>The Two Solutions: Optimistic vs Pessimistic Locking</h2>
<p>When I hit this problem, I had two choices. Let me explain both properly.</p>
<hr />
<h3>Option 1 — Optimistic Locking</h3>
<p>Optimistic locking assumes conflicts are <strong>rare</strong>. It doesn't lock the row upfront. Instead, it tracks a <strong>version number</strong> on every row. When you update a row, you check that the version hasn't changed since you read it. If it has, someone else updated it first — your transaction fails and you retry.</p>
<p><strong>Database level:</strong></p>
<pre><code class="language-sql">ALTER TABLE promo_codes ADD COLUMN version BIGINT DEFAULT 0;
</code></pre>
<p><strong>JPA entity:</strong></p>
<pre><code class="language-java">@Entity
@Table(name = "promo_codes")
public class PromoCode {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Version  // ← This is all it takes
    private Long version;

    private int usedCount;
    private Integer usageLimit;

    // ... other fields
}
</code></pre>
<p><strong>How it works at runtime:</strong></p>
<pre><code class="language-plaintext">User A reads:  { id: 1, used_count: 0, version: 0 }
User B reads:  { id: 1, used_count: 0, version: 0 }

User A updates:
  UPDATE promo_codes
  SET used_count = 1, version = 1
  WHERE id = 1 AND version = 0  ← checks version matches
  → 1 row affected ✅

User B updates:
  UPDATE promo_codes
  SET used_count = 1, version = 1
  WHERE id = 1 AND version = 0  ← version is now 1, not 0!
  → 0 rows affected ❌
  → JPA throws OptimisticLockException
</code></pre>
<p><strong>In Spring Boot:</strong></p>
<pre><code class="language-java">@Service
public class OrderServiceImpl {

    @Transactional
    public OrderResponse placeOrder(Long userId, PlaceOrderRequest request) {
        try {
            PromoCode promo = promoCodeRepository
                .findByCodeIgnoreCase(request.getPromoCode())
                .orElseThrow(() -&gt; new BusinessException("Invalid promo code"));

            if (!promo.isApplicable(subtotal)) {
                throw new BusinessException("Promo code not applicable");
            }

            promo.incrementUsage();
            promoCodeRepository.save(promo); // throws OptimisticLockException if version mismatch

            // ... rest of order placement

        } catch (OptimisticLockException | ObjectOptimisticLockingFailureException e) {
            throw new BusinessException(
                "This promo code was just used by someone else. Please try again.",
                "PROMO_CONFLICT",
                HttpStatus.CONFLICT
            );
        }
    }
}
</code></pre>
<p><strong>Pros:</strong></p>
<ul>
<li><p>No database locks held — very high throughput</p>
</li>
<li><p>Scales well with many concurrent users</p>
</li>
<li><p>Works well when conflicts are genuinely rare</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p>User gets an error and must retry</p>
</li>
<li><p>Requires retry logic or clear user messaging</p>
</li>
<li><p>Can fail repeatedly under very high contention</p>
</li>
</ul>
<hr />
<h3>Option 2 — Pessimistic Locking</h3>
<p>Pessimistic locking assumes conflicts are <strong>likely</strong>. It locks the row at the database level the moment you read it. Every other transaction that tries to read that row must wait until the lock is released.</p>
<pre><code class="language-java">public interface PromoCodeRepository extends JpaRepository&lt;PromoCode, Long&gt; {

    // Normal read — no lock
    Optional&lt;PromoCode&gt; findByCodeIgnoreCase(String code);

    // Locked read — for use during actual order placement
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT p FROM PromoCode p WHERE LOWER(p.code) = LOWER(:code)")
    Optional&lt;PromoCode&gt; findByCodeForUpdate(@Param("code") String code);
}
</code></pre>
<p><strong>How it works at runtime:</strong></p>
<pre><code class="language-plaintext">User A calls findByCodeForUpdate("EMRAS20")
  → PostgreSQL executes: SELECT ... FOR UPDATE
  → Row is LOCKED

User B calls findByCodeForUpdate("EMRAS20")
  → PostgreSQL: row is locked, B must WAIT

User A validates → increments usedCount → commits
  → LOCK RELEASED

User B continues (lock released)
  → reads updated row: used_count = 1, limit = 1
  → isUsageLimitReached() = true
  → throws BusinessException: "This promo code has reached its usage limit"
  → User B gets a proper error message ✅
</code></pre>
<p><strong>In Spring Boot — full implementation:</strong></p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final PromoCodeRepository promoCodeRepository;

    @Transactional  // ← IMPORTANT: lock is held for the entire transaction
    public OrderResponse placeOrder(Long userId, PlaceOrderRequest request) {

        // ... validate cart, address, payment method ...

        BigDecimal subtotal = cart.getSubtotal();
        BigDecimal discountAmount = BigDecimal.ZERO;
        PromoCode promoCode = null;

        if (request.getPromoCode() != null &amp;&amp; !request.getPromoCode().isBlank()) {

            // Use the LOCKED version — not the regular findByCodeIgnoreCase
            promoCode = promoCodeRepository
                .findByCodeForUpdate(request.getPromoCode())  // ← SELECT ... FOR UPDATE
                .orElseThrow(() -&gt; new BusinessException(
                    "Invalid promo code",
                    "PROMO_INVALID",
                    HttpStatus.BAD_REQUEST
                ));

            // At this point, the row is locked.
            // Any concurrent request trying to use this promo code WAITS here.

            if (promoCode.isExpired()) {
                throw new BusinessException(
                    "This promo code has expired",
                    "PROMO_EXPIRED",
                    HttpStatus.BAD_REQUEST
                );
            }

            if (promoCode.isUsageLimitReached()) {
                // This is what User B sees after User A commits
                throw new BusinessException(
                    "This promo code has reached its usage limit",
                    "PROMO_LIMIT_REACHED",
                    HttpStatus.BAD_REQUEST
                );
            }

            if (subtotal.compareTo(promoCode.getMinOrderAmount()) &lt; 0) {
                throw new BusinessException(
                    "Minimum order amount not met for this promo code",
                    "PROMO_MIN_NOT_MET",
                    HttpStatus.BAD_REQUEST
                );
            }

            discountAmount = promoCode.calculateDiscount(subtotal);
        }

        // ... build order, deduct stock, save order ...

        // Increment usage BEFORE committing
        // Lock is released when @Transactional commits at method end
        if (promoCode != null) {
            promoCode.incrementUsage();
            promoCodeRepository.save(promoCode);
        }

        cartFacade.clearCart(userId);
        return toResponse(order);

        // Transaction commits here → lock on promo_codes row is released
    }
}
</code></pre>
<p><strong>Key insight:</strong> The <code>@Transactional</code> boundary is everything. The lock is held from <code>findByCodeForUpdate</code> until the method returns and the transaction commits. During that entire window, no other transaction can read or modify that row.</p>
<p><strong>Pros:</strong></p>
<ul>
<li><p>Guarantees exactly one winner — no retry needed</p>
</li>
<li><p>Simple to reason about — the second user always gets a clear error</p>
</li>
<li><p>No version column needed in the schema</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p>Locks are held longer — lower throughput under high load</p>
</li>
<li><p>Risk of deadlocks if multiple resources are locked in wrong order</p>
</li>
<li><p>Not ideal for very high traffic (thousands of concurrent checkouts)</p>
</li>
</ul>
<hr />
<h2>How I Chose: Why Pessimistic Locking Was Right for Emras</h2>
<p>This is the question I sat with for a while. Both are valid — the choice depends on your system's characteristics.</p>
<p>Here's the decision framework I used:</p>
<h3>1. How frequent are conflicts?</h3>
<p>Promo codes with low usage limits (1, 5, 10 uses) will have frequent conflicts during promotions. Optimistic locking would cause many users to get retry errors during a flash sale — bad UX.</p>
<p><strong>→ Pessimistic locking wins</strong> for low-limit promo codes.</p>
<h3>2. How long is the critical section?</h3>
<p>In <code>placeOrder</code>, we lock the promo code row but the method also deducts stock, saves the order, and clears the cart. The lock is held for maybe 50-100ms in total. That's acceptable — we're not doing any slow external API calls inside the transaction.</p>
<p><strong>→ Pessimistic locking is safe</strong> when the transaction is short.</p>
<h3>3. What's the user experience on failure?</h3>
<p>With optimistic locking, User B gets: <em>"Conflict — please retry."</em> With pessimistic locking, User B gets: <em>"This promo code has reached its usage limit."</em></p>
<p>The second message is clearer and more honest.</p>
<p><strong>→ Pessimistic locking wins</strong> on UX.</p>
<h3>4. What's the expected scale?</h3>
<p>Emras is a clothing store for Bangladesh — not Amazon. Thousands of concurrent checkouts at the exact same millisecond is not a realistic scenario. If we ever scale to that level, we'd introduce a distributed job queue (like Redis + Kafka) for promo code redemption anyway.</p>
<p><strong>→ Pessimistic locking is appropriate</strong> at this scale.</p>
<h3>The Rule of Thumb</h3>
<pre><code class="language-plaintext">Low conflict probability + High throughput needed  →  Optimistic Locking
High conflict probability + Correctness is critical →  Pessimistic Locking
</code></pre>
<p>For financial data, inventory, and promo codes — <strong>correctness always beats throughput</strong>. Use pessimistic locking.</p>
<hr />
<h2>The Other Race Condition I Found: Stock Deduction</h2>
<p>Once I started thinking about promo codes, I checked our stock deduction logic too.</p>
<p>Our <code>ProductVariantRepository</code> had this query:</p>
<pre><code class="language-java">@Modifying
@Query("""
    UPDATE ProductVariant pv
    SET pv.stockQuantity = pv.stockQuantity - :qty
    WHERE pv.id = :id
    AND pv.stockQuantity &gt;= :qty
    """)
int deductStock(@Param("id") Long id, @Param("qty") int qty);
</code></pre>
<p>This is actually <strong>safe by design</strong> — it's an atomic SQL operation. The <code>AND pv.stockQuantity &gt;= :qty</code> check and the decrement happen in the same statement at the database level. PostgreSQL handles this atomically.</p>
<p>If stock is 1 and two users both try to buy 1 unit:</p>
<pre><code class="language-plaintext">User A: UPDATE ... WHERE stockQuantity &gt;= 1 → stockQuantity = 0 → 1 row affected ✅
User B: UPDATE ... WHERE stockQuantity &gt;= 1 → stockQuantity = 0, fails condition → 0 rows affected
</code></pre>
<p>We check the return value (0 rows = conflict) and throw an exception:</p>
<pre><code class="language-java">@Override
@Transactional
public void deductStock(Long variantId, int quantity) {
    int rowsUpdated = variantRepository.deductStock(variantId, quantity);
    if (rowsUpdated == 0) {
        throw new BusinessException(
            "Insufficient stock for the requested quantity",
            "CART_INSUFFICIENT_STOCK",
            HttpStatus.CONFLICT
        );
    }
}
</code></pre>
<p><strong>This is the correct way to handle stock deduction</strong> — single atomic SQL statement, no separate read-then-write, no locking needed.</p>
<hr />
<h2>What I Learned</h2>
<p>Building Emras taught me that race conditions are invisible until they cost you. Here's what I took away:</p>
<p><strong>1. Always ask: "What if two requests hit this at the same time?"</strong> For any write operation involving shared data (promo codes, stock, wallet balance, seat reservations), this question is mandatory.</p>
<p><strong>2. Prefer atomic SQL operations where possible</strong> <code>UPDATE ... WHERE quantity &gt;= required</code> is safer than <code>SELECT → check → UPDATE</code> because it reduces the window for race conditions.</p>
<p><strong>3. For resource reservation, pessimistic locking is your friend</strong> When the business impact of a conflict is high (double-spending promo codes, overselling inventory), pay the small throughput cost for correctness.</p>
<p><strong>4. Keep transactions short when holding locks</strong> Don't make external API calls inside a transaction that holds a database lock. Send emails after commit, not during.</p>
<p><strong>5. Optimistic locking shines for profile updates, settings, non-critical data</strong> If two users update their profile bio at the same time, an optimistic lock conflict is fine — one just retries. The stakes are low.</p>
<hr />
<h2>Final Architecture</h2>
<p>Here's how the complete promo code flow looks in our final implementation:</p>
<pre><code class="language-plaintext">POST /api/v1/orders
        ↓
OrderController.placeOrder()
        ↓
@Transactional OrderServiceImpl.placeOrder()
        │
        ├── cartFacade.isCartEmpty()        → CartServiceImpl
        ├── cartFacade.getCartSummary()     → CartServiceImpl
        │
        ├── promoCodeRepository             ← SELECT FOR UPDATE (row locked)
        │     .findByCodeForUpdate()           No other transaction can touch this row
        │
        ├── promo.isApplicable() checks     ← if any fail, exception thrown, lock released
        │
        ├── productFacade.deductStock()     ← atomic UPDATE WHERE qty &gt;= required
        │
        ├── orderRepository.save()          ← INSERT order + items
        │
        ├── promoCode.incrementUsage()      ← UPDATE promo, version++
        │     promoCodeRepository.save()
        │
        ├── cartFacade.clearCart()          ← cart emptied
        │
        └── COMMIT                          ← lock released here
</code></pre>
<p>Clean, safe, and correct.</p>
<hr />
<h2>Conclusion</h2>
<p>The Emras promo code race condition was a reminder that correctness in concurrent systems requires deliberate design. It doesn't fix itself, and it doesn't always fail loudly.</p>
<p>If you're building any system where:</p>
<ul>
<li><p>Multiple users can claim a limited resource simultaneously</p>
</li>
<li><p>Financial data is involved</p>
</li>
<li><p>Inventory can be oversold</p>
</li>
</ul>
<p>Stop and ask yourself: <em>"What happens if two requests arrive at the same millisecond?"</em></p>
<p>Then pick your locking strategy accordingly.</p>
]]></content:encoded></item><item><title><![CDATA[Supercharging Spring Boot with Redis: Caching, Pub/Sub, Rate Limiting & More]]></title><description><![CDATA[From ~500ms PostgreSQL queries to ~1ms Redis hits — a complete hands-on guide

If you've ever watched your API response times crawl under load and thought "there must be a better way" — Redis is that ]]></description><link>https://rifat-tipu.hashnode.dev/supercharging-spring-boot-with-redis-caching-pub-sub-rate-limiting-more</link><guid isPermaLink="true">https://rifat-tipu.hashnode.dev/supercharging-spring-boot-with-redis-caching-pub-sub-rate-limiting-more</guid><dc:creator><![CDATA[Rifat Hossain]]></dc:creator><pubDate>Mon, 22 Jun 2026 08:55:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a38f0e29103525e06ad8540/b38af20b-d306-4029-bd4d-b03b2fb0d043.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>From ~500ms PostgreSQL queries to ~1ms Redis hits — a complete hands-on guide</em></p>
<hr />
<p>If you've ever watched your API response times crawl under load and thought <em>"there must be a better way"</em> — Redis is that better way. In this post, I'll walk you through a production-ready Spring Boot project I built to explore everything Redis can do: declarative caching, raw data structures, Pub/Sub messaging, rate limiting, and session management.</p>
<p>By the end, you'll understand not just <em>how</em> to wire Redis into Spring Boot, but <em>why</em> each piece works — and what traps to avoid along the way.</p>
<p>🔗 <strong>Full source code:</strong> <a href="https://github.com/Rifat-Tipu/Redis_Implementation">github.com/Rifat-Tipu/Redis_Implementation</a></p>
<hr />
<h2>What Are We Building?</h2>
<p>A Spring Boot app backed by <strong>PostgreSQL</strong> (source of truth) and <strong>Redis</strong> (cache layer), with a live benchmark endpoint that shows you the speed difference in real time:</p>
<table>
<thead>
<tr>
<th>Feature</th>
<th>What it demonstrates</th>
</tr>
</thead>
<tbody><tr>
<td>🐘 PostgreSQL + JPA</td>
<td>Real persistence via Spring Data repository</td>
</tr>
<tr>
<td>⚡ <code>@Cacheable</code></td>
<td>Declarative caching with per-cache TTLs</td>
</tr>
<tr>
<td>⏱️ Benchmark endpoint</td>
<td>~500ms (DB) vs ~1ms (Redis) — live proof</td>
</tr>
<tr>
<td>🗄️ Data Structures</td>
<td>String, Hash, List, Set, Sorted Set via <code>RedisTemplate</code></td>
</tr>
<tr>
<td>📡 Pub/Sub</td>
<td>Real-time messaging between services</td>
</tr>
<tr>
<td>🛡️ Rate Limiting</td>
<td>Atomic sliding-window counter</td>
</tr>
<tr>
<td>🔧 Session Management</td>
<td>Spring Session backed by Redis</td>
</tr>
</tbody></table>
<hr />
<h2>A Quick Redis Primer</h2>
<p><strong>Redis</strong> (Remote Dictionary Server) is an open-source, <strong>in-memory data store</strong> that doubles as a cache, message broker, and streaming engine. Its key strengths:</p>
<ul>
<li><p>⚡ <strong>Blazing fast</strong> — data lives in RAM, sub-millisecond latency</p>
</li>
<li><p>🧩 <strong>Rich data types</strong> — Strings, Hashes, Lists, Sets, Sorted Sets</p>
</li>
<li><p>⏰ <strong>TTL support</strong> — keys auto-expire (perfect for caches and sessions)</p>
</li>
<li><p>📡 <strong>Pub/Sub</strong> — native publish/subscribe messaging</p>
</li>
<li><p>🔒 <strong>Atomic operations</strong> — thread-safe commands enable safe concurrency</p>
</li>
</ul>
<hr />
<h2>Architecture Overview</h2>
<p>The request flow is straightforward: every read hits Redis first. Only on a cache miss does the app fall through to PostgreSQL.</p>
<pre><code class="language-plaintext">CLIENT (Browser / curl)
    │
    ▼
┌─────────────────────────────────────────────────────────────┐
│                      Spring Boot App                         │
│                                                             │
│  ┌──────────────────┐     ┌─────────────────────────────┐  │
│  │    Controller    │────▶│     Redis (Cache Layer)      │  │
│  │    Service       │◀────│  @Cacheable / CacheManager   │  │
│  │  @Cacheable etc. │     └─────────────────────────────┘  │
│  └────────┬─────────┘       ↑ HIT: return instantly        │
│           │                  ↓ MISS: continue to DB below   │
│  ┌────────▼─────────┐                                       │
│  │   Repository     │  ← Spring Data JPA                    │
│  └────────┬─────────┘                                       │
└───────────┼─────────────────────────────────────────────────┘
            ▼
   ┌─────────────────┐
   │   PostgreSQL    │  ← Source of truth
   └─────────────────┘
</code></pre>
<hr />
<h2>Project Structure</h2>
<pre><code class="language-plaintext">spring-boot-redis/
├── src/main/java/com/example/redis/
│   ├── config/
│   │   ├── RedisConfig.java           # RedisTemplate + CacheManager beans
│   │   └── PubSubConfig.java          # Channel topics + listener container
│   ├── model/
│   │   └── Product.java               # JPA @Entity
│   ├── repository/
│   │   └── ProductRepository.java     # Spring Data JPA
│   ├── service/
│   │   ├── ProductService.java        # @Cacheable / @CachePut / @CacheEvict
│   │   ├── RedisDataStructureService.java
│   │   ├── RateLimitService.java
│   │   ├── RedisMessagePublisher.java
│   │   └── RedisMessageSubscriber.java
│   └── controller/
│       ├── ProductController.java
│       ├── BenchmarkController.java   # ⏱️ The star of the show
│       ├── RedisController.java
│       └── PubSubController.java
├── docker-compose.yml
└── pom.xml
</code></pre>
<hr />
<h2>Quick Start</h2>
<h3>Prerequisites</h3>
<ul>
<li>Java 17+, Maven 3.8+, Docker</li>
</ul>
<h3>1. Clone and spin up infrastructure</h3>
<pre><code class="language-bash">git clone https://github.com/Rifat-Tipu/Redis_Implementation.git
cd Redis_Implementation
docker-compose up -d
</code></pre>
<p>This brings up:</p>
<ul>
<li><p><strong>PostgreSQL</strong> on <code>localhost:5432</code></p>
</li>
<li><p><strong>Redis</strong> on <code>localhost:6379</code></p>
</li>
<li><p><strong>Redis Commander</strong> (web UI) on <code>http://localhost:8081</code></p>
</li>
</ul>
<h3>2. Run the app</h3>
<pre><code class="language-bash">mvn spring-boot:run
</code></pre>
<h3>3. See the speed difference live</h3>
<pre><code class="language-bash"># First call — cache MISS, hits PostgreSQL
curl http://localhost:8080/api/benchmark/product/1
# { "source": "PostgreSQL (cache MISS)", "timeMs": 504 }

# Second call — cache HIT, served from Redis
curl http://localhost:8080/api/benchmark/product/1
# { "source": "Redis (cache HIT)", "timeMs": 2 }
</code></pre>
<p>That 250x speed difference isn't a trick. It's just what happens when RAM replaces disk I/O.</p>
<hr />
<h2>Core Concept 1: Declarative Caching with <code>@Cacheable</code></h2>
<p>Spring's caching abstraction makes caching <strong>declarative</strong> — just annotate your service method:</p>
<pre><code class="language-java">@Cacheable(value = "products", key = "#id")
public Optional&lt;Product&gt; findById(Long id) {
    // This method body only executes on cache MISS
    // On HIT, Spring returns the cached value and skips this entirely
    return productRepository.findById(id);
}
</code></pre>
<h3>The Cache Miss → Hit Lifecycle</h3>
<p><strong>First call (MISS):</strong></p>
<ol>
<li><p>Spring checks Redis for key <code>products::1</code> → not found</p>
</li>
<li><p>Method executes, hits PostgreSQL</p>
</li>
<li><p>Result stored in Redis with a 10-minute TTL</p>
</li>
<li><p>Response returned to client</p>
</li>
</ol>
<p><strong>Second call (HIT):</strong></p>
<ol>
<li><p>Spring checks Redis for key <code>products::1</code> → found!</p>
</li>
<li><p><strong>PostgreSQL is never called.</strong> The method body never runs.</p>
</li>
<li><p>Cached result returned in ~1ms</p>
</li>
</ol>
<h3>The Three Caching Annotations</h3>
<table>
<thead>
<tr>
<th>Annotation</th>
<th>When to use</th>
<th>Behavior</th>
</tr>
</thead>
<tbody><tr>
<td><code>@Cacheable</code></td>
<td>Read operations</td>
<td>Return cached value if present; else execute + cache result</td>
</tr>
<tr>
<td><code>@CachePut</code></td>
<td>Update operations</td>
<td>Always execute; always update the cache</td>
</tr>
<tr>
<td><code>@CacheEvict</code></td>
<td>Delete / write ops</td>
<td>Execute + remove the stale cache entry</td>
</tr>
</tbody></table>
<pre><code class="language-java">// Update: refresh cache after saving
@CachePut(value = "products", key = "#id")
public Optional&lt;Product&gt; update(Long id, Product p) { ... }

// Delete: remove from cache after deleting from DB
@CacheEvict(value = "products", key = "#id")
public boolean delete(Long id) { ... }

// Nuclear option: clear everything
@CacheEvict(value = "products", allEntries = true)
public void clearCache() { ... }
</code></pre>
<h3>Cache Key Strategies</h3>
<table>
<thead>
<tr>
<th>Expression</th>
<th>Resolves to</th>
</tr>
</thead>
<tbody><tr>
<td><code>key = "#id"</code></td>
<td>Method parameter <code>id</code></td>
</tr>
<tr>
<td><code>key = "'all-products'"</code></td>
<td>The literal string <code>all-products</code></td>
</tr>
<tr>
<td><code>key = "#product.id"</code></td>
<td>The <code>id</code> field on a parameter object</td>
</tr>
</tbody></table>
<blockquote>
<p>💡 <strong>The hardest part of caching isn't getting data in — it's knowing when to evict.</strong> A stale list cache is a subtle bug that's easy to miss. Use <code>@CacheEvict</code> on every write path.</p>
</blockquote>
<hr />
<h2>Core Concept 2: RedisTemplate &amp; Data Structures</h2>
<p>When you need direct control beyond <code>@Cacheable</code>, <code>RedisTemplate&lt;String, Object&gt;</code> gives you the full Redis API:</p>
<pre><code class="language-java">redisTemplate.opsForValue()   // Strings
redisTemplate.opsForHash()    // Hashes
redisTemplate.opsForList()    // Lists
redisTemplate.opsForSet()     // Sets
redisTemplate.opsForZSet()    // Sorted Sets
</code></pre>
<h3>Strings — counters, tokens, simple values</h3>
<pre><code class="language-java">// Store with TTL
redisTemplate.opsForValue().set("session:abc", userObject, Duration.ofMinutes(30));

// Atomic counter — thread-safe, no locking needed
Long views = redisTemplate.opsForValue().increment("page:views");
</code></pre>
<p><strong>Best for:</strong> page counters, feature flags, session tokens, cached primitives</p>
<h3>Hashes — field/value maps</h3>
<pre><code class="language-java">redisTemplate.opsForHash().putAll("user:42", Map.of(
    "name",  "Alice",
    "email", "alice@example.com",
    "role",  "ADMIN"
));

// Read a single field without fetching the whole object
String email = (String) redisTemplate.opsForHash().get("user:42", "email");
</code></pre>
<p><strong>Best for:</strong> user profiles, configuration objects, anything with partial updates</p>
<h3>Lists — ordered sequences</h3>
<pre><code class="language-java">// Queue (FIFO): push right, pop left
redisTemplate.opsForList().rightPush("task:queue", task);
Object next = redisTemplate.opsForList().leftPop("task:queue");

// Stack (LIFO): push right, pop right
Object latest = redisTemplate.opsForList().rightPop("events");
</code></pre>
<p><strong>Best for:</strong> job queues, activity feeds, recent items</p>
<h3>Sets — unique, unordered</h3>
<pre><code class="language-java">redisTemplate.opsForSet().add("online:users", "alice", "bob", "carol");
boolean isOnline = redisTemplate.opsForSet().isMember("online:users", "alice");
</code></pre>
<p><strong>Best for:</strong> unique visitor tracking, tag systems, membership checks</p>
<h3>Sorted Sets — scored rankings</h3>
<pre><code class="language-java">redisTemplate.opsForZSet().add("leaderboard", "Alice", 9500);
redisTemplate.opsForZSet().add("leaderboard", "Bob",   12000);

// Top 3, highest score first
Set&lt;Object&gt; top3 = redisTemplate.opsForZSet().reverseRange("leaderboard", 0, 2);

// Atomically increment score
redisTemplate.opsForZSet().incrementScore("leaderboard", "Alice", 500);
</code></pre>
<p><strong>Best for:</strong> leaderboards, priority queues, time-series data</p>
<hr />
<h2>Core Concept 3: Publish / Subscribe</h2>
<p>Redis Pub/Sub lets services broadcast messages to each other without direct coupling. The publisher fires and forgets; all active subscribers receive it.</p>
<pre><code class="language-plaintext">Publisher ──convertAndSend()──▶ Redis Channel ──▶ Subscriber.receiveMessage()
</code></pre>
<p><strong>Publisher:</strong></p>
<pre><code class="language-java">redisTemplate.convertAndSend("notifications", "Order #1234 placed!");
</code></pre>
<p><strong>Subscriber</strong> (Spring wires this up automatically):</p>
<pre><code class="language-java">public void receiveMessage(String message) {
    log.info("Received: {}", message);
    // forward, persist, or trigger downstream logic
}
</code></pre>
<p><strong>Configuration:</strong></p>
<pre><code class="language-java">container.addMessageListener(listenerAdapter, new ChannelTopic("notifications"));
</code></pre>
<blockquote>
<p>⚠️ <strong>Important gotcha:</strong> Redis Pub/Sub is fire-and-forget. If no subscriber is online when you publish, the message is <strong>gone</strong>. For guaranteed delivery, use Redis Streams or move to RabbitMQ/Kafka.</p>
</blockquote>
<p>Real-world uses: stock price updates, live notifications, cache invalidation signals, microservice event broadcasting.</p>
<hr />
<h2>Core Concept 4: Rate Limiting</h2>
<p>The Redis-idiomatic rate limiter uses an atomic counter with a sliding TTL window:</p>
<pre><code class="language-plaintext">Request arrives
     │
     ▼
INCR rate_limit:{clientId}   ← atomic, thread-safe
     │
     ├── If count == 1 → EXPIRE rate_limit:{clientId} 60  (start the window)
     │
     └── If count ≤ 10 → ✅ Allow
         If count &gt;  10 → ❌ 429 Too Many Requests
</code></pre>
<pre><code class="language-java">public boolean isAllowed(String clientId) {
    String key   = "rate_limit:" + clientId;
    Long   count = redisTemplate.opsForValue().increment(key);

    if (count == 1) {
        redisTemplate.expire(key, 60, TimeUnit.SECONDS);
    }
    return count &lt;= 10;
}
</code></pre>
<p>Why not <code>GET → check → SET</code>? Because that has a <strong>race condition</strong> — two concurrent requests can both read <code>9</code>, both think they're allowed, and both increment to <code>10</code>. <code>INCR</code> is atomic at the Redis level: each call returns the post-increment value, exactly once, even under concurrent load.</p>
<hr />
<h2>Configuration Reference</h2>
<pre><code class="language-properties"># PostgreSQL
spring.datasource.url=jdbc:postgresql://localhost:5432/redis_demo
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.jpa.show-sql=true          # On cache HIT, you'll see zero SQL here

# Redis
spring.data.redis.host=localhost
spring.data.redis.port=6379

# Connection pool (Lettuce)
spring.data.redis.lettuce.pool.max-active=10
spring.data.redis.lettuce.pool.min-idle=1

# Cache TTL (10 minutes)
spring.cache.redis.time-to-live=600000

# Spring Session
spring.session.store-type=redis
spring.session.timeout=30m
</code></pre>
<p>💡 Enable <code>spring.jpa.show-sql=true</code> during development — on a cache hit, you'll see <strong>no SQL queries at all</strong>. That's the most visceral way to confirm Redis is doing its job.</p>
<hr />
<h2>Key Learnings from Building This</h2>
<p><strong>1. Cache invalidation is the hard part.</strong> Getting data into Redis is trivial. The real skill is deciding when to evict — especially for list caches, where a single item change makes the whole list stale.</p>
<p><strong>2. Serialization matters more than you think.</strong> Java's default serialization makes keys unreadable in Redis Commander and breaks silently if you rename a class. Switch to <code>Jackson2JsonRedisSerializer</code> from day one.</p>
<p><strong>3.</strong> <code>INCR + EXPIRE</code> <strong>is the elegant rate limiter pattern.</strong> My first attempt with <code>GET → check → SET</code> had a race condition under load. The <code>INCR</code>-first pattern is inherently atomic.</p>
<p><strong>4. Pub/Sub is fire-and-forget by design.</strong> Don't use it where message durability matters. Use Redis Streams or a proper message broker instead.</p>
<p><strong>5. Connection pooling matters in production.</strong> Lettuce's defaults aren't tuned for high concurrency. Set <code>max-active</code> and <code>max-wait</code> before you go live, or you'll hit connection exhaustion at the worst moment.</p>
<hr />
<h2>REST API Summary</h2>
<h3>Benchmark</h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>Endpoint</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>GET</code></td>
<td><code>/api/benchmark/product/{id}</code></td>
<td>Returns source + time taken</td>
</tr>
<tr>
<td><code>DELETE</code></td>
<td><code>/api/benchmark/product/{id}/cache</code></td>
<td>Evict from Redis (reset demo)</td>
</tr>
</tbody></table>
<h3>Products (CRUD with caching)</h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>Endpoint</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>GET</code></td>
<td><code>/api/products/{id}</code></td>
<td>Cached individually</td>
</tr>
<tr>
<td><code>POST</code></td>
<td><code>/api/products</code></td>
<td>Creates + evicts list cache</td>
</tr>
<tr>
<td><code>PUT</code></td>
<td><code>/api/products/{id}</code></td>
<td>Updates + refreshes cache</td>
</tr>
<tr>
<td><code>DELETE</code></td>
<td><code>/api/products/{id}</code></td>
<td>Deletes + evicts cache</td>
</tr>
</tbody></table>
<h3>Redis Data Structures</h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>Endpoint</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>POST/GET</code></td>
<td><code>/api/redis/string/{key}</code></td>
<td>String operations</td>
</tr>
<tr>
<td><code>POST/GET</code></td>
<td><code>/api/redis/hash/{key}</code></td>
<td>Hash operations</td>
</tr>
<tr>
<td><code>POST/GET</code></td>
<td><code>/api/redis/list/{key}</code></td>
<td>List operations</td>
</tr>
<tr>
<td><code>POST/GET</code></td>
<td><code>/api/redis/set/{key}</code></td>
<td>Set operations</td>
</tr>
<tr>
<td><code>POST/GET</code></td>
<td><code>/api/redis/leaderboard/{key}/top/{n}</code></td>
<td>Sorted set leaderboard</td>
</tr>
</tbody></table>
<h3>Pub/Sub &amp; Rate Limiting</h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>Endpoint</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>POST</code></td>
<td><code>/api/pubsub/publish</code></td>
<td>Publish a message</td>
</tr>
<tr>
<td><code>GET</code></td>
<td><code>/api/rate-limit/check?clientId=x</code></td>
<td>Check limit status</td>
</tr>
</tbody></table>
<hr />
<h2>Tech Stack</h2>
<ul>
<li><p><strong>Spring Boot 3.2</strong> — application framework</p>
</li>
<li><p><strong>PostgreSQL 15</strong> — relational database (source of truth)</p>
</li>
<li><p><strong>Spring Data Redis + Lettuce</strong> — non-blocking Redis client</p>
</li>
<li><p><strong>Spring Cache</strong> — declarative caching (<code>@Cacheable</code> etc.)</p>
</li>
<li><p><strong>Spring Session</strong> — Redis-backed distributed sessions</p>
</li>
<li><p><strong>Jackson</strong> — JSON serialization for Redis values</p>
</li>
<li><p><strong>Docker Compose</strong> — PostgreSQL + Redis + Redis Commander</p>
</li>
</ul>
<hr />
<h2>Wrapping Up</h2>
<p>Redis isn't just a cache — it's a toolkit. Once you understand the data structures and their semantics, you start seeing use cases everywhere: rate limiting, leaderboards, session stores, real-time messaging, job queues.</p>
<p>The benchmark endpoint is the most satisfying part of this project. Watching a request drop from 504ms to 2ms on the second call makes the theory click in a way that docs never quite do.</p>
<p>If you found this useful, the full source is at <a href="https://github.com/Rifat-Tipu/Redis_Implementation">github.com/Rifat-Tipu/Redis_Implementation</a>. Feel free to clone it, break things, and experiment.</p>
<hr />
<p><em>Tags:</em> <code>redis</code> <code>spring-boot</code> <code>java</code> <code>caching</code> <code>backend</code> <code>tutorial</code></p>
]]></content:encoded></item></channel></rss>