# Redis Stream Demo Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build an independent Spring Boot demo project that imports `redis-stream-springboot-starter` and verifies real Redis Stream publish/subscribe behavior. **Architecture:** The demo is a downstream Maven Spring Boot application under `C:\code\hatch\redis-stream-starter-demo`. It depends on the starter from the local Maven repository, sends a `DemoMessage` with `RedisStreamEnhanceTemplate`, and consumes it with an `EnhanceStreamConsumerHandler` implementation. Integration testing uses the provided Redis service when reachable and skips when unreachable. **Tech Stack:** Java 8, Maven, Spring Boot `2.3.12.RELEASE`, JUnit 5, `redis-stream-springboot-starter:1.0.0-SNAPSHOT`, Redis Stream. --- ## File Structure - Create: `C:\code\hatch\redis-stream-starter-demo\pom.xml` - Maven project definition, Spring Boot 2.3.12 dependency management, starter dependency, test setup. - Create: `C:\code\hatch\redis-stream-starter-demo\.gitignore` - Keeps build output and IDE files out of the demo project. - Create: `C:\code\hatch\redis-stream-starter-demo\src\main\resources\application.yml` - Redis host/port defaults and overridable password environment variable. - Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoApplication.java` - Spring Boot entry point. - Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoMessage.java` - Demo message payload extending `StreamBaseMessage`. - Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoRuntimeInbox.java` - In-memory queue shared by runtime logging and integration tests. - Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoProducer.java` - Sends demo messages through the starter template. - Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoConsumer.java` - Starter consumer handler for `demo-topic`. - Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoStartupRunner.java` - Sends one demo message when running the app manually. - Create: `C:\code\hatch\redis-stream-starter-demo\src\test\java\com\njcn\demo\redisstream\DemoRoundTripIntegrationTest.java` - Real Redis publish/subscribe integration test. ## Task 1: Prepare Build Scaffolding **Files:** - Create: `C:\code\hatch\redis-stream-starter-demo\pom.xml` - Create: `C:\code\hatch\redis-stream-starter-demo\.gitignore` - Create: `C:\code\hatch\redis-stream-starter-demo\src\main\resources\application.yml` - [ ] **Step 1: Install the starter into the local Maven repository** Run from the starter repository: ```bash mvn install ``` Expected: `BUILD SUCCESS`, with `redis-stream-springboot-starter-1.0.0-SNAPSHOT.jar` installed into the local Maven repository. - [ ] **Step 2: Create demo project directories** Run: ```powershell New-Item -ItemType Directory -Force ` 'C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream', ` 'C:\code\hatch\redis-stream-starter-demo\src\main\resources', ` 'C:\code\hatch\redis-stream-starter-demo\src\test\java\com\njcn\demo\redisstream' ``` Expected: all three directories exist. - [ ] **Step 3: Create `pom.xml`** Write this exact content: ```xml 4.0.0 com.njcn.demo redis-stream-starter-demo 1.0.0-SNAPSHOT jar redis-stream-starter-demo Demo application for redis-stream-springboot-starter 1.8 8 8 UTF-8 UTF-8 2.3.12.RELEASE org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import com.njcn redis-stream-springboot-starter 1.0.0-SNAPSHOT org.springframework.boot spring-boot-starter-test test org.apache.maven.plugins maven-compiler-plugin 3.8.1 1.8 1.8 UTF-8 org.apache.maven.plugins maven-surefire-plugin 2.22.2 org.springframework.boot spring-boot-maven-plugin ${spring-boot.version} ``` - [ ] **Step 4: Create `.gitignore`** Write this exact content: ```gitignore target/ .idea/ *.iml .classpath .project .settings/ ``` - [ ] **Step 5: Create `application.yml`** Write this exact content: ```yaml spring: redis: host: ${DEMO_REDIS_HOST:192.168.1.22} port: ${DEMO_REDIS_PORT:12379} password: ${DEMO_REDIS_PASSWORD:} redis-stream: env-isolation: true env: demo consumer-name: demo-consumer read-count: 10 block-ms: 500 max-retry: 1 ``` - [ ] **Step 6: Verify build scaffolding resolves dependencies** Run from the demo project: ```bash mvn test -DskipTests ``` Expected: `BUILD SUCCESS`. ## Task 2: Write the Round-Trip Integration Test First **Files:** - Create: `C:\code\hatch\redis-stream-starter-demo\src\test\java\com\njcn\demo\redisstream\DemoRoundTripIntegrationTest.java` - [ ] **Step 1: Write the failing test** Write this exact content: ```java package com.njcn.demo.redisstream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ConditionEvaluationResult; import org.junit.jupiter.api.extension.ExecutionCondition; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtensionContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.connection.stream.RecordId; import java.net.InetSocketAddress; import java.net.Socket; import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @ExtendWith(DemoRoundTripIntegrationTest.RedisReachableCondition.class) @SpringBootTest class DemoRoundTripIntegrationTest { @Autowired private DemoProducer producer; @Autowired private DemoRuntimeInbox inbox; @Test void publishesAndConsumesDemoMessage() throws Exception { inbox.clear(); String payload = "integration-" + System.currentTimeMillis(); DemoMessage sent = new DemoMessage(); sent.setPayload(payload); sent.setSource("integration-test"); sent.setTag("demo"); RecordId recordId = producer.send(sent); DemoMessage received = inbox.poll(10, TimeUnit.SECONDS); assertNotNull(recordId, "XADD should return a Redis stream record id"); assertNotNull(received, "consumer should receive the sent message within 10 seconds"); assertEquals(sent.getKey(), received.getKey(), "message key should round-trip"); assertEquals(payload, received.getPayload(), "payload should round-trip"); assertEquals("integration-test", received.getSource(), "source should round-trip"); } static class RedisReachableCondition implements ExecutionCondition { @Override public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { String host = System.getProperty("spring.redis.host", System.getenv().getOrDefault("DEMO_REDIS_HOST", "192.168.1.22")); int port = Integer.parseInt(System.getProperty("spring.redis.port", System.getenv().getOrDefault("DEMO_REDIS_PORT", "12379"))); try (Socket socket = new Socket()) { socket.connect(new InetSocketAddress(host, port), 500); return ConditionEvaluationResult.enabled("redis reachable at " + host + ":" + port); } catch (Exception e) { return ConditionEvaluationResult.disabled("redis unreachable at " + host + ":" + port); } } } } ``` - [ ] **Step 2: Run the test and verify it fails because application classes do not exist** Run from the demo project with the Redis password supplied through the process environment: ```bash mvn test -Dtest=DemoRoundTripIntegrationTest ``` Expected: compilation failure mentioning missing symbols such as `DemoProducer`, `DemoRuntimeInbox`, and `DemoMessage`. ## Task 3: Implement Minimal Demo Application Classes **Files:** - Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoApplication.java` - Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoMessage.java` - Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoRuntimeInbox.java` - Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoProducer.java` - Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoConsumer.java` - [ ] **Step 1: Create `DemoApplication.java`** Write this exact content: ```java package com.njcn.demo.redisstream; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } ``` - [ ] **Step 2: Create `DemoMessage.java`** Write this exact content: ```java package com.njcn.demo.redisstream; import com.njcn.middle.stream.domain.StreamBaseMessage; public class DemoMessage extends StreamBaseMessage { private String payload; public String getPayload() { return payload; } public void setPayload(String payload) { this.payload = payload; } } ``` - [ ] **Step 3: Create `DemoRuntimeInbox.java`** Write this exact content: ```java package com.njcn.demo.redisstream; import org.springframework.stereotype.Component; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @Component public class DemoRuntimeInbox { private final BlockingQueue messages = new LinkedBlockingQueue<>(); public void add(DemoMessage message) { messages.add(message); } public DemoMessage poll(long timeout, TimeUnit unit) throws InterruptedException { return messages.poll(timeout, unit); } public void clear() { messages.clear(); } } ``` - [ ] **Step 4: Create `DemoProducer.java`** Write this exact content: ```java package com.njcn.demo.redisstream; import com.njcn.middle.stream.template.RedisStreamEnhanceTemplate; import org.springframework.data.redis.connection.stream.RecordId; import org.springframework.stereotype.Service; @Service public class DemoProducer { public static final String TOPIC = "demo-topic"; private final RedisStreamEnhanceTemplate streamTemplate; public DemoProducer(RedisStreamEnhanceTemplate streamTemplate) { this.streamTemplate = streamTemplate; } public RecordId send(DemoMessage message) { return streamTemplate.send(TOPIC, message, false); } } ``` - [ ] **Step 5: Create `DemoConsumer.java`** Write this exact content: ```java package com.njcn.demo.redisstream; import com.njcn.middle.stream.handler.EnhanceStreamConsumerHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Component public class DemoConsumer extends EnhanceStreamConsumerHandler { public static final String GROUP = "demo-group"; private static final Logger log = LoggerFactory.getLogger(DemoConsumer.class); private final DemoRuntimeInbox inbox; public DemoConsumer(DemoRuntimeInbox inbox) { this.inbox = inbox; } @Override public String topic() { return DemoProducer.TOPIC; } @Override public String group() { return GROUP; } @Override public Class messageType() { return DemoMessage.class; } @Override public void handleMessage(DemoMessage message) { log.info("Consumed Redis Stream demo message: key={}, payload={}, source={}, tag={}", message.getKey(), message.getPayload(), message.getSource(), message.getTag()); inbox.add(message); } @Override public boolean isRetry() { return false; } @Override public boolean throwException() { return false; } } ``` - [ ] **Step 6: Run the integration test and verify it passes** Run from the demo project with the Redis password supplied through the process environment: ```bash mvn test -Dtest=DemoRoundTripIntegrationTest ``` Expected: `BUILD SUCCESS`, and the test reports one successful test when Redis is reachable. ## Task 4: Add Manual Startup Publisher **Files:** - Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoStartupRunner.java` - [ ] **Step 1: Write the failing startup-runner test by extending the integration test** Modify `DemoRoundTripIntegrationTest.java` to include this field and test method: ```java @Autowired private DemoStartupRunner startupRunner; @Test void startupRunnerPublishesDemoMessage() throws Exception { inbox.clear(); startupRunner.run(); DemoMessage received = inbox.poll(10, TimeUnit.SECONDS); assertNotNull(received, "startup runner should publish a message"); assertEquals("startup-runner", received.getSource(), "startup runner should set source"); assertEquals("hello redis stream starter", received.getPayload(), "startup runner payload should match"); } ``` The full test class should still keep the first `publishesAndConsumesDemoMessage` test unchanged. - [ ] **Step 2: Run the test and verify it fails because `DemoStartupRunner` does not exist** Run: ```bash mvn test -Dtest=DemoRoundTripIntegrationTest ``` Expected: compilation failure mentioning missing symbol `DemoStartupRunner`. - [ ] **Step 3: Create `DemoStartupRunner.java`** Write this exact content: ```java package com.njcn.demo.redisstream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.data.redis.connection.stream.RecordId; import org.springframework.stereotype.Component; @Component public class DemoStartupRunner implements ApplicationRunner { private static final Logger log = LoggerFactory.getLogger(DemoStartupRunner.class); private final DemoProducer producer; public DemoStartupRunner(DemoProducer producer) { this.producer = producer; } @Override public void run(ApplicationArguments args) { run(); } public RecordId run() { DemoMessage message = new DemoMessage(); message.setPayload("hello redis stream starter"); message.setSource("startup-runner"); message.setTag("demo"); RecordId recordId = producer.send(message); log.info("Published Redis Stream demo message: recordId={}, key={}, payload={}", recordId, message.getKey(), message.getPayload()); return recordId; } } ``` - [ ] **Step 4: Run the integration test and verify both tests pass** Run: ```bash mvn test -Dtest=DemoRoundTripIntegrationTest ``` Expected: `BUILD SUCCESS`, with two successful tests when Redis is reachable. ## Task 5: Final Verification **Files:** - No new files. - [ ] **Step 1: Run the starter build** Run from the starter repository: ```bash mvn install ``` Expected: `BUILD SUCCESS`. - [ ] **Step 2: Run all demo tests** Run from the demo project with the Redis password supplied through the process environment: ```bash mvn test ``` Expected: `BUILD SUCCESS`. - [ ] **Step 3: Run the demo application** Run from the demo project with the Redis password supplied through the process environment: ```bash mvn spring-boot:run ``` Expected: startup logs show a published Redis Stream message and a consumed Redis Stream message. Stop the process after the message is consumed. - [ ] **Step 4: Capture final file status** Run from the starter repository: ```bash git status --short ``` Expected: only the implementation plan file is uncommitted unless execution commits it separately. ## Self-Review - Spec coverage: Tasks cover local starter installation, independent demo project creation, Redis configuration, producer, consumer, startup publishing, integration testing, and manual runtime verification. - Placeholder scan: No TBD/TODO markers are used. Redis password is intentionally not written into this committed plan. - Type consistency: `DemoMessage`, `DemoProducer`, `DemoConsumer`, `DemoRuntimeInbox`, `DemoStartupRunner`, and `DemoRoundTripIntegrationTest` use matching package names and method signatures.