Files
redis-stream-springboot-sta…/docs/superpowers/plans/2026-06-25-redis-stream-demo-implementation.md

596 lines
19 KiB
Markdown
Raw Normal View History

# 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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.njcn.demo</groupId>
<artifactId>redis-stream-starter-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>redis-stream-starter-demo</name>
<description>Demo application for redis-stream-springboot-starter</description>
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.12.RELEASE</spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>redis-stream-springboot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>
</project>
```
- [ ] **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<DemoMessage> 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<DemoMessage> {
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<DemoMessage> 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.