1. Repo restructure (example repo)
- 1.1 Move existing
src/toapp/src/, move relevant Gradle config intoapp/build.gradle.kts, keep rootbuild.gradle.ktsfor sharedsubprojects {}config (toolchain, Spotless, repositories) - 1.2 Add
readiness-gate/module skeleton; updatesettings.gradle.ktswithinclude("readiness-gate", "app") - 1.3 Update Dockerfile,
.dockerignore, Makefile paths (run/dev/test/build/up/imagetargets) for the new module layout - 1.4 Run
./gradlew buildand existing tests; confirm green baseline before adding Kafka code
2. Readiness-gate library
- 2.1 Add dependencies to
readiness-gate/build.gradle.kts— deviation: no Kafka client dependency. The library ended up transport-agnostic (annotation + AOP + health indicator only), so it stays reusable outside Kafka contexts;appsupplies the only Kafka-specific wiring. - 2.2 Implement
@ConfigConsumerannotation andConsumptionTracker(last-consumed-time + error state per tracked consumer) - 2.3 Implement the “fully consumed” check — deviation: not
AvailabilityChangeEvent.SpringApplicationpublishes its ownAvailabilityChangeEvent(ACCEPTING_TRAFFIC)right afterApplicationReadyEvent, which raced and overwrote a custom one. Used aHealthIndicatorin thereadinesshealth group instead (ConfigConsumptionHealthIndicator), which Kubernetes’ probe polls directly — no race, no extra scheduler. - 2.4 Gated-scheduled-job pattern — implemented as a direct
tracker.isFullyConsumed()check at the top of the job method (CacheSnapshotJob), documented pattern rather than a@GatedScheduledmarker annotation. - 2.5 Unit tests (
ConsumptionTrackerTest): readiness stays DOWN while messages/errors keep arriving, flips UP after the quiet period with no errors — covers both spec scenarios.
3. App: Kafka consumption + local cache
- 3.1 Kafka config — deviation:
app.kafka.*properties + a hand-writtenKafkaConfig(ConsumerFactory/ConcurrentKafkaListenerContainerFactorybeans), notspring.kafka.*. Spring Boot 4.1 ships no Kafka autoconfiguration module yet, so there’s nospring.kafka.*binding to lean on. - 3.2 Implement thread-safe
LocalConfigCache(ConcurrentHashMap-backed) - 3.3 Implement
@KafkaListener(annotated@ConfigConsumer) that upserts non-null records into the cache and removes entries on tombstone (null-value) records - 3.4 Implement dummy
@Scheduledjob reading fromLocalConfigCache, gated on readiness - 3.5 Implement HTTP endpoint returning data sourced from
LocalConfigCache - 3.6 Integration test (Testcontainers Kafka, KRaft mode) covering: cache populated after consumption, tombstone removes entry, endpoint reflects cache contents —
ConfigTopicCacheIntegrationTest, full:app:checkgreen.
4. Containerization
- 4.1 Update multi-stage
Dockerfileto build:app:bootJar(transitively building:readiness-gate) and produce the runtime image - 4.2 Extend
docker-compose.ymlwith a local single-broker KRaft Kafka (confluentinc/cp-kafka:7.6.0,.withKraft()-equivalent env) + kafka-ui + akafka-topic-initone-shot service, for fast local iteration outside minikube - 4.3 Verified
docker build+ fulldocker compose --profile full up --build: app connects to Kafka, consumes seed data,/actuator/health/readinessand/api/cacheboth confirmed working end to end
5. Kubernetes manifests (minikube)
- 5.1 Kafka KRaft Deployment + Service manifest (single broker,
KAFKA_PROCESS_ROLES=broker,controller, pinnedconfluentinc/cp-kafka:7.6.0—apache/kafka:3.9.0was tried first but its entrypoint failed to compute a routableadvertised.listenersin this environment, both via Testcontainers and docker-compose) - 5.2 kafka-ui Deployment + Service manifest, pointed at the broker
- 5.3 ConfigMap supplying
CONSUMER_GROUP_PREFIX; pod-unique suffix via Downward API (metadata.uid→POD_UID) on the app Deployment; combined via Spring property placeholder resolution - 5.4
kafka-topic-initJob: waits for broker readiness, creates compacted topic (cleanup.policy=compact), seeds sample key/value records via Kafka CLI, seed data shared with docker-compose’s seed file - 5.5 App Deployment (3 replicas) + Service — deviation:
NodePort, not LoadBalancer.minikube service app -n local-cache-demo --urlworks directly with NodePort, nominikube tunnelprocess required — simpler for a reader following along. - 5.6
kustomization.yamltying manifests together forkubectl apply -k k8s/, plus apostgres.yaml+db-secret.yamlnot in the original plan (the app inherits a hard Postgres/Liquibase dependency from the base template — needed in the k8s demo too, or app pods crash-loop)
6. Build/deploy automation
- 6.1 Makefile targets:
demo-build,demo-image(docker build +minikube image load),demo-deploy(kubectl apply -k+ wait for topic-init Job + app rollout),demo-down - 6.2 Top-level
demo-uptarget chaining build → image → deploy, printing both the app and kafka-ui URLs - 6.3 Manually validate end-to-end on a real minikube cluster — not done in this session (no minikube/kubectl available in this sandbox). Validated as much as possible without one:
kubectl kustomize/kubeconform confirm all 13 rendered resources are schema-valid, and the equivalent flow (Kafka KRaft + kafka-ui + topic-init + app, readiness gate, cache endpoint, tombstone) was verified end-to-end viadocker compose --profile full. Runmake demo-upon a real minikube before publishing to confirm.
7. Example repo README
- 7.1 Quick-start section (minikube prerequisites/resources, one-command deploy)
- 7.2 ConfigMap → env var → Spring property consumer-group templating flow (Downward API vs. UUID-env-var tradeoff)
- 7.3 Curl instructions for the cache-backed endpoint, including exposing the Service via minikube
- 7.4 How to observe per-pod consumer groups and the compacted topic in kafka-ui
- 7.5 Tombstone/delete behavior (verified the exact
kafka-console-producer --property null.marker=...command against a live broker) - 7.6 Scaling replicas to observe broadcast behavior
8. Publish example repo
- 8.1 Confirmed with user — a GitHub remote already existed (
javaAndScriptDeveloper/kafka-backed-local-read-replica-article, matching the expected naming convention), so no new repo needed. - 8.2 Push example repo to GitHub — user explicitly declined (“don’t touch git here”) when asked. All changes are committed-ready but left in the local working tree; user will commit/push themselves. The article’s “Code” link points at the existing (not-yet-pushed) remote URL — push before publishing.
9. Article
- 9.1 Front matter (
title,date,tags,excerpt,description,tech_icon,faq) perlocal-cache-kafka-articlespec - 9.2 Introduction: Reddit-discussion framing + link to the source thread
- 9.3 TL;DR block
- 9.4 Body sections covering all required topics, each grounded in explicit answers to the four Reddit questions (dedicated “Answering the four questions” section)
- 9.5 Three Mermaid diagrams: topology/broadcast, readiness sequence, tombstone/delete flow
- 9.6 Code snippets copied from the finished example repo, verified verbatim against source (imports/Javadoc trimmed, matching site convention)
- 9.7 Pros/cons vs Redis section (table)
- 9.8 Closing “Code” section linking to the GitHub repo (not yet pushed — see 8.2)
- 9.9 Proofread against both specs’ scenarios; verified snippet-to-source traceability
- 9.10
bundle exec jekyll buildcheck: post renders at/2026/08/08/local-cache-via-kafka/, FAQ JSON-LD + accordion present, all 3 mermaid blocks present, tutorials-page card renders, internal anchors resolve