CodeGym /Kursy /Docker for Spring /Redis after a stable app + postgres

Redis after a stable app + postgres

Docker for Spring
Poziom 19, Lekcja 0
Dostępny

1. A stable foundation: app+postgres

When you add a new infrastructure dependency (Redis, a message broker, anything like that), you are not just “starting one more container”. You increase the number of places where something can go wrong: network, configuration, profiles, readiness, ports, image versions, startup order. If the base stack is still shaky, the new dependency turns diagnosis into a detective story with no ending: the clues are there, the culprit is unknown.

In a Docker/Compose environment, there is an unpleasant bit of math: the more services you have, the more combinations of problems you get. While we had only one container, app, everything was relatively linear: “starts / does not start”. When postgres appeared, it became interesting: “is the application not starting because of the profile? because of the host? because of the password? because of readiness? because of migration?”. That is normal, but it requires discipline.

If we had pulled Redis in earlier, at the moment when app + postgres had not yet been brought to the point of “starts up reliably”, you would get the perfect educational disaster. The application crashes, and you do not understand why: did it fail to connect to the database, did the migrations not run, is Redis unavailable, or did you simply write localhost by accident. That kind of learning setup usually ends with the student “defeating” the error by randomly changing things, but they cannot explain what they actually did. And here we are training the opposite skill: explainability and reproducibility.

You can think of it like renovating an apartment. First you make the walls level and the wiring safe, and only then do you buy a smart home setup and lighting that changes color with your mood. If you start with the lighting, it will blink beautifully against crooked walls — but living there is still hard.

What does “stable app + postgres” mean?

“Stable” does not mean “it started once after three hours of pain, and now I am afraid to touch the computer”. In the context of the course, stability means the ability to repeat the startup and get the same result, and when something breaks, quickly understand the class of the problem. That is what turns a local environment into a development tool instead of a fragile personal artifact on one laptop.

We want app + postgres to be predictable in three dimensions: startup, data, and observability. Startup means the application does not “overtake” the database and fail on the attempt to connect. Data means migrations are applied, the schema is in the expected shape, and reset works the way you understand it (and not “something was deleted, but I do not know what”). Observability means that logs and health endpoints show what is happening, and you are not diagnosing the system by facial expression.

To make this a bit more tangible, let us capture what a “healthy stack” looks like, not as a list of incantations, but as a table of observable signals.

What we check Where we see it What “ok” looks like
PostgreSQL is actually ready docker compose ps, health status, postgres logs status
healthy
, no endless restarts in the logs
The application starts after the DB is ready app logs no endless Connection refused, a successful startup is visible
Flyway applied migrations app logs you can see that migrations were applied, the application does not fail on Validate failed
The API responds
GET /api/catalog/items
GET /actuator/health
HTTP 200, health is green
The environment reset is clear docker compose down vs down -v you understand when data is preserved and when you intentionally wipe it

Notice: there are no “magical expectations” here, like “let it be fast” or “let it be pretty”. Stability is about being able to bring the environment up tomorrow, on another machine, on another OS, and get the same properties.

And only after this foundation does it make sense to add Redis. Because Redis is a layer on top of reads, and reads should already know how to reach the source of truth. Caching something that cannot be read reliably is like taping a leaky pipe and celebrating that the water now flows “a little differently”.

2. Redis as a cache layer

When a beginner first hears “Redis”, they often imagine “a very fast database that will replace PostgreSQL and make everything lightning fast”. That is not entirely false, but in the context of our course this is too broad a view — and it gets in the way. Today Redis is needed not as a new world, but as an infrastructure extension: we add it to the Compose stack and use it as a cache for repeated reads.

A cache is a saved result of a read operation that can be returned without going back to the main storage. In our project, the main storage in postgres mode is PostgreSQL. It remains the source of truth: if Redis disappears tomorrow, the business data should not disappear with it. That is an important point: Redis stores derived data, not primary data.

To avoid confusion, it helps to keep the roles apart, especially in a learning project:

Question PostgreSQL (in postgres mode) Redis (in cache mode)
What it stores primary business data for the catalog read results (cache)
What happens if you delete it you lose the data (and that hurts) the cache disappears, but is restored by repeated reads
Why it is needed data correctness and consistency faster repeated reads and less load on the main storage
How it is “enabled” postgres profile, datasource config, migrations cache profile, Redis host/port, caching on read methods

This view helps keep the level from getting overcomplicated. We are not building a “Redis-centric architecture” and we are not turning Container-Ready Catalog Service into a Redis textbook. We add Redis the way it often appears in real teams: after the database, as the next infrastructure layer, enabled through a profile so it does not break the main path.

3. Cache hit / cache miss

Caching sounds like magic: “saved it — and now it is fast”. But it becomes understandable the moment you learn to say two words: cache miss and cache hit. A miss is when the cache does not yet have the needed result, and we have to go to the main storage. A hit is when the result is already in the cache, and we return it without going to the database.

The key detail is the cache key, that is, the value we use to look up an entry in the cache. For reading an item by id, that is almost always the id itself. For reading “the whole list”, the key is effectively the same because the query is the same. What matters right now is the mechanism itself: same query → same key → chance of a hit.

Let us look at a minimal model in our domain, still without Spring annotations and without Redis — just so the brain can see the “mechanics” instead of the “magic words”.

import com.example.catalog.catalog.model.CatalogItem;

// Two identical reads in a row: from the storage point of view, these are two separate requests
CatalogItem first = storage.findById(10L);
CatalogItem second = storage.findById(10L); // the second time we go to the main storage again

// The data is the same, but the work is done twice
System.out.println(first.getSku());   // for example: "SKU-10"
System.out.println(second.getSku());  // for example: "SKU-10"

Here both reads are identical in meaning, but from the application’s point of view they are two independent trips to storage. If storage under the hood is PostgreSQL, then we are asking the database to do the work twice. Cache appears exactly at the point where we ask: “can we avoid going there again?”.

Now a simplified “manual” cache model (yes, this is not production code, but it is honest mechanics):

import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

class SimpleItemCache {
    // Local in-process cache: key is the item id, value is the item itself
    private final Map<Long, CatalogItem> cache = new HashMap<>();

    CatalogItem get(Long id, Supplier<CatalogItem> loader) {
        // If the record is missing (miss), we call loader (a trip to the main storage) and put it into the cache
        // If the record exists (hit), we return it without calling loader
        return cache.computeIfAbsent(id, key -> loader.get());
    }
}

In this model, id is the cache key. If id has not been seen yet, we call loader, get the data from the main storage, and store it in the cache. If it has been seen, we return it from Map. That is what “hit/miss” looks like in its pure form, without any Redis or Spring involved.

The same idea can be visualized as a simple flow:

flowchart TD
    A[Request: item id=10] --> B{Is it in cache?}
    B -- no --> C[Read from PostgreSQL]
    C --> D[Put the result into cache]
    D --> E[Return response]
    B -- yes --> E[Return response from cache]

And this is where an important “anti-expectation” appears, the one that breaks beginners’ mental model: the first request is almost always a miss. The speedup is usually visible on repeated reads. That is why in our learning day we choose repeated read scenarios: so that hit/miss is visible not “somewhere in theory”, but in behavior.

At the same time, cache is not free. It adds questions: “when do we clear it?”, “what do we do when the price changes?”, “what if the cache becomes stale?”. We are consciously not turning that into a separate architecture course today. But one rule is worth remembering now: caching is easiest to start with simple, frequent reads, not with operations that actively change data.

4. The first read scenarios for cache

It is very tempting to cache everything at once: the list, the item, exports, statistics, the weather in the author’s city, and the Bitcoin price. That is a normal desire; it is called the “big toggle syndrome: enable acceleration”. But in a real project, and especially in a learning one, you must not do that, because you lose the cause-and-effect relationship. We need to see the effect of the cache on a clear, small area.

In Container-Ready Catalog Service there are two natural candidates that do not require redesigning the domain and are easy to verify with simple HTTP requests. They are reading the catalog item list and reading an item by its identifier. They are as “clean” as possible: these are read operations that can be repeated twice in a row in exactly the same way and honestly show “the first time is expensive, the second time is easy”.

A mini model of repeated reading by id looks like this (in service-layer terms):

import com.example.catalog.catalog.model.CatalogItem;

// Read the same id twice: for the cache, this is the same key
CatalogItem a = queryService.findById(10L);
CatalogItem b = queryService.findById(10L); // key = 10L

// The result is the same, but in the second call we expect a "hit"
System.out.println(a.getTitle()); // for example: "USB-C Cable"
System.out.println(b.getTitle()); // for example: "USB-C Cable"

For the list, the logic is similar: if the read method is “the same one”, then the caching idea is clear — do not recalculate the same thing twice.

import java.util.List;

// Repeat the same read scenario: such a query usually has the same cache key
List<CatalogItem> first = queryService.findAll();
List<CatalogItem> second = queryService.findAll(); // the same read scenario again

Why do we limit ourselves to two scenarios? Because right now we care not about maximal cache coverage, but about controlled expansion of the Compose stack and a careful configuration model. If we start caching half the application, we will spend the whole day discussing “why did this not update” and accidentally make cache the central topic. Right now it is more important to see the cache layer itself on a small, repeatable area.

5. The order of adding Redis

In the early infrastructure steps there is a very common trap: “let us bring up the whole zoo at once”. It looks like progress: lots of containers, lots of logs, the feeling of adult development. But in practice it is often just a faster way to get chaos. We deliberately build the stack in layers: each layer must be understood before the next one appears.

If app + postgres is unstable, caching gives you no engineering value. First, because there is nothing to cache while reads from the source of truth are not working predictably. Second, because you cannot honestly interpret the symptoms. The application crashed — is the database not ready? did the migration break? is Redis unavailable? is the profile wrong? is the port wrong? And each new service adds at least one new class of problems: network, config, readiness.

There is another practical point: cache is not just an “accelerator”. Cache is an agreement that the result of a read can be reused. If you have not yet established discipline around migrations and reset/recovery, you can easily end up in a situation where part of the data “apparently” remained (in the cache), but the database has already been reset, and you start diagnosing ghosts. That is why we first stabilized the database lifecycle, and only then add the layer that stores derived results.

The order here is not accidental. First you need the source of truth and its lifecycle, then cache as an accelerator for repeated reads, and only after that does it make sense to expand the stack further.

6. Redis in the Compose stack

Now that the cache idea and the reason “why not earlier” have settled in, it becomes clear that Compose is not waiting for a revolution, but for a careful extension: to our already predictable stand app + postgres we will add a third service — Redis. The most important thing that does not change is the main principle of the course: the same application image runs differently only because of runtime configuration.

At the stack architecture level, this looks very calm: the client still sends HTTP to app, the application still stores data in PostgreSQL, and Redis appears as a layer that sometimes responds faster because the result is already “at hand”. If Redis is unavailable (for example, you did not start it), the ideal learning service should behave predictably: either do not enable cache mode, or clearly report that the cache dependency is not satisfied. “Silently working however it wants” is a bad scenario.

Schematically, our world becomes this:

flowchart LR
    Client[HTTP client] --> App[Spring Boot app]
    App -->|source of truth| PG[(PostgreSQL)]
    App -->|cache layer| R[(Redis)]

The value of this step for the Docker course is that you practice a very practical skill: expanding the environment without breaking the base path. We do not create a separate “Redis image”, we do not write “docker mode code”, and we do not replace half the configs. We add the dependency the way you will do it in a real project later: the service appears in Compose, the address comes through the service name, enabling happens through a profile, and observability comes through healthchecks and logs.

From here on, what we need is not another general discussion of cache benefits, but very practical wiring: minimally enabling caching in Spring and connecting Redis as a containerized dependency. We already have the foundation for this step: stable app + postgres, readiness, and a clear environment lifecycle.

7. Typical mistakes on Redis day one

The first attempts to add Redis are almost always accompanied by the feeling, “I followed the instructions, and it did not magically speed up”. That is normal: cache is something you need to observe, not “believe in”. If you already understand why Redis is added right now and what role it plays, most of the pitfalls can be avoided without pain at all (or at least stepped on in soft slippers).

Mistake No. 1: adding Redis when app + postgres is still unstable.
If your base stack comes up only every other time, Redis will not accelerate anything except the appearance of new errors. In that situation, you will be fixing the database readiness, migrations, and Redis connection at the same time, and your brain will quickly switch into “I do not understand anything” mode. The right order is to first achieve a repeatable startup of app + postgres, and only then expand.

Mistake No. 2: thinking that Redis replaces PostgreSQL.
On our day, Redis is a cache, not the source of truth. If you start treating it like a “second database”, you will very quickly run into data that is “not right”, “disappeared”, or “did not update”. The source of truth remains in PostgreSQL, and Redis stores a derived read result that can be recreated.

Mistake No. 3: trying to cache everything at once to “definitely feel the effect”.
The wider you spread caching, the harder it is to understand what exactly you are observing. In a learning project this is especially critical: we want to see hit/miss on two specific read scenarios, not build a mini-course on cache invalidation and data consistency.

Mistake No. 4: expecting a cache hit while checking different requests.
If you read id=10 once and then id=11 the second time, those are two different keys, and a true hit will not happen there. Likewise, you cannot compare “the first time I requested the list without filters, the second time with a filter” and be surprised that the cache did not work. For a smoke check, you need an identical repeated read.

Mistake No. 5: waiting for acceleration on the first request.
The first request is usually a miss: you need to go to PostgreSQL and only then store the result. If you evaluate cache by the “feeling of speed” on the first call, you will almost certainly draw the wrong conclusion. Cache shows up on repeated reads, and that is normal: it does not predict the future, it remembers the past.

1
Zadanie
Docker for Spring, poziom 19, lekcja 0
Niedostępne
Ponowne odczytywanie z PostgreSQL bez cache
Ponowne odczytywanie z PostgreSQL bez cache
1
Zadanie
Docker for Spring, poziom 19, lekcja 0
Niedostępne
Dwa powtarzalne scenariusze odczytu przed pojawieniem się Redis
Dwa powtarzalne scenariusze odczytu przed pojawieniem się Redis
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION