Repository: floci-io/floci (2026-04-08T22:56:02Z)

Issue:
[FEAT] DDB streams persistence
## Service

DynamoDBStreams

## API Action / Feature

ListStreams

## AWS Documentation

https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_streams_ListStreams.html

## Why is this needed?

DynamoDBStreamService doesn't read from the persisted files to populate its cache, so after restarting floci, the ListStreams endpoint returns an empty list. (This may be more of a bug than a new feature? Unsure on that point.)

Possibly DynamoDBService should "enable" any existing enabled streams when rehydrating its caches?

## Are you willing to contribute a PR?

- [ X] Yes (but i'm new to this stack and likely to be slow)
- [ ] No


Discussion:
Hi @bkindersleytrulioo, thanks for taking the time to report this! 

I assigned it to you, take your time and let me know if you need something. 

Thank you again!

@hectorvent i don't seem to have permission to push a branch

```
git push -u origin feat/ddbstream-persist
ERROR: Permission to floci-io/floci.git denied to bkindersleytrulioo.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
```

is this a setting on your side, or more likely a configuration issue on my side?
@bkindersleytrulioo you will need to do:

1. create a floci fork
2. do a branch in your floci fork repo
3. Open the PR to the official floci repo

Hope this steps help!
:tada: This issue has been resolved in version 1.5.0 :tada:

The release is available on [GitHub release](https://github.com/floci-io/floci/releases/tag/1.5.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:



Patch:
diff --git a/src/main/java/io/github/hectorvent/floci/services/dynamodb/DynamoDbStreamService.java b/src/main/java/io/github/hectorvent/floci/services/dynamodb/DynamoDbStreamService.java
index 10a0dbcd..d5b3e9f3 100644
--- a/src/main/java/io/github/hectorvent/floci/services/dynamodb/DynamoDbStreamService.java
+++ b/src/main/java/io/github/hectorvent/floci/services/dynamodb/DynamoDbStreamService.java
@@ -1,6 +1,8 @@
 package io.github.hectorvent.floci.services.dynamodb;
 
 import io.github.hectorvent.floci.core.common.AwsException;
+import io.github.hectorvent.floci.core.storage.StorageBackend;
+import io.github.hectorvent.floci.core.storage.StorageFactory;
 import io.github.hectorvent.floci.services.dynamodb.model.DynamoDbStreamRecord;
 import io.github.hectorvent.floci.services.dynamodb.model.KeySchemaElement;
 import io.github.hectorvent.floci.services.dynamodb.model.StreamDescription;
@@ -8,6 +10,7 @@
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.core.type.TypeReference;
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
 import org.jboss.logging.Logger;
@@ -18,6 +21,7 @@
 import java.util.ArrayList;
 import java.util.Base64;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedDeque;
@@ -42,20 +46,50 @@ public class DynamoDbStreamService {
     private final ObjectMapper objectMapper;
 
     @Inject
-    public DynamoDbStreamService(ObjectMapper objectMapper) {
+    public DynamoDbStreamService(ObjectMapper objectMapper, StorageFactory storageFactory) {
+        this(objectMapper, storageFactory.create("dynamodb", "dynamodb-tables.json",
+                new TypeReference<Map<String, TableDefinition>>() {}));
+    }
+
+    /** Package-private constructor for testing. */
+    DynamoDbStreamService(ObjectMapper objectMapper, StorageBackend<String, TableDefinition> tableStore) {
         this.objectMapper = objectMapper;
+        loadPersistedStreams(tableStore);
+    }
+
+
+    private void loadPersistedStreams(StorageBackend<String, TableDefinition> tableStore) {
+        if (tableStore == null) return;
+        for (String tableKey : tableStore.keys()){
+                String region = tableKey.split("::", 2)[0];
+                tableStore.get(tableKey).ifPresent(table -> {
+                        if (!table.isStreamEnabled()) return;
+                        this.enableStream(table.getTableName(), table.getTableArn(), table.getStreamViewType(), region, table.getStreamArn());
+                    });
+        }
     }
 
     public StreamDescription enableStream(String tableName, String tableArn, String viewType, String region) {
+        return enableStream(tableName, tableArn, viewType, region, null);
+    }
+
+    public StreamDescription enableStream(String tableName, String tableArn, String viewType, String region, String streamArnInput) {
         String key = streamKey(region, tableName);
         StreamDescription existing = streams.get(key);
         if (existing != null && "ENABLED".equals(existing.getStreamStatus())) {
             return existing;
         }
 
+        String streamArn = streamArnInput;
         Instant now = Instant.now();
-        String label = STREAM_LABEL_FORMAT.format(now);
-        String streamArn = tableArn + "/stream/" + label;
+        String label;
+        if (streamArn == null){
+            label = STREAM_LABEL_FORMAT.format(now);
+            streamArn = tableArn + "/stream/" + label;
+        }
+        else {
+            label = streamArn.split("/stream/", 2)[1];
+        }
 
         StreamDescription sd = new StreamDescription();
         sd.setStreamArn(streamArn);


Test patch:
diff --git a/src/test/java/io/github/hectorvent/floci/services/dynamodb/DynamoDbStreamServiceTest.java b/src/test/java/io/github/hectorvent/floci/services/dynamodb/DynamoDbStreamServiceTest.java
new file mode 100644
index 00000000..6b0c559a
--- /dev/null
+++ b/src/test/java/io/github/hectorvent/floci/services/dynamodb/DynamoDbStreamServiceTest.java
@@ -0,0 +1,55 @@
+package io.github.hectorvent.floci.services.dynamodb;
+
+import io.github.hectorvent.floci.core.common.AwsException;
+import io.github.hectorvent.floci.core.storage.InMemoryStorage;
+import io.github.hectorvent.floci.core.storage.StorageBackend;
+import io.github.hectorvent.floci.services.dynamodb.model.AttributeDefinition;
+import io.github.hectorvent.floci.services.dynamodb.model.KeySchemaElement;
+import io.github.hectorvent.floci.services.dynamodb.model.StreamDescription;
+import io.github.hectorvent.floci.services.dynamodb.model.TableDefinition;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class DynamoDbStreamServiceTest {
+
+    private DynamoDbStreamService service;
+    private ObjectMapper mapper;
+    private StorageBackend<String, TableDefinition> storage;
+
+    @BeforeEach
+    void setUp() {
+        mapper = new ObjectMapper();
+        storage =  new InMemoryStorage<>();
+        TableDefinition table = createTestTableWithStream();
+        storage.put("us-east-1::" + table.getTableName(), table);
+        service = new DynamoDbStreamService(mapper, storage);
+    }
+
+    private TableDefinition createTestTableWithStream() {
+        var tableDef = new TableDefinition("TestTable", 
+                List.of(new KeySchemaElement("userId", "HASH")),
+                List.of(new AttributeDefinition("userId", "S")),
+                "us-east-1", "000000000000");
+        tableDef.setStreamEnabled(true);
+        tableDef.setStreamArn("arn:aws:dynamodb:us-west-2:000000000000:table/TestTable/stream/2026-04-08T15:24:10.801");
+        return tableDef;
+    }
+
+    @Test
+    void loadsStreamOnStartup() {
+        var streams = service.listStreams(null, null);
+        assertEquals(1, streams.size());
+        StreamDescription stream = streams.get(0);
+        assertEquals("TestTable", stream.getTableName());
+        assertEquals("ENABLED", stream.getStreamStatus());
+    }
+
+}