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

19 KiB

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:

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:

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 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:

target/
.idea/
*.iml
.classpath
.project
.settings/
  • Step 5: Create application.yml

Write this exact content:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

    @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:

mvn test -Dtest=DemoRoundTripIntegrationTest

Expected: compilation failure mentioning missing symbol DemoStartupRunner.

  • Step 3: Create DemoStartupRunner.java

Write this exact content:

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:

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:

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:

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:

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:

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.