# 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.0com.njcn.demoredis-stream-starter-demo1.0.0-SNAPSHOTjarredis-stream-starter-demoDemo application for redis-stream-springboot-starter1.888UTF-8UTF-82.3.12.RELEASEorg.springframework.bootspring-boot-dependencies${spring-boot.version}pomimportcom.njcnredis-stream-springboot-starter1.0.0-SNAPSHOTorg.springframework.bootspring-boot-starter-testtestorg.apache.maven.pluginsmaven-compiler-plugin3.8.11.81.8UTF-8org.apache.maven.pluginsmaven-surefire-plugin2.22.2org.springframework.bootspring-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.