@@ -25,7 +27,7 @@
* - Event handler method has invalid signature
* - Reflection issues when accessing event handlers
*/
-public class EventHandlerException extends RuntimeException {
+public class EventHandlerException extends FireflyException {
public EventHandlerException(String message) {
super(message);
diff --git a/src/main/java/org/fireflyframework/eventsourcing/config/EventSourcingAutoConfiguration.java b/src/main/java/org/fireflyframework/eventsourcing/config/EventSourcingAutoConfiguration.java
index 9002911..cb0aa74 100644
--- a/src/main/java/org/fireflyframework/eventsourcing/config/EventSourcingAutoConfiguration.java
+++ b/src/main/java/org/fireflyframework/eventsourcing/config/EventSourcingAutoConfiguration.java
@@ -17,20 +17,29 @@
package org.fireflyframework.eventsourcing.config;
import com.fasterxml.jackson.databind.ObjectMapper;
+import io.micrometer.core.instrument.MeterRegistry;
import org.fireflyframework.eda.publisher.EventPublisherFactory;
+import org.fireflyframework.eventsourcing.monitoring.EventStoreMetrics;
+import org.fireflyframework.eventsourcing.outbox.EventOutboxProcessor;
+import org.fireflyframework.eventsourcing.outbox.EventOutboxRepository;
+import org.fireflyframework.eventsourcing.outbox.EventOutboxService;
import org.fireflyframework.eventsourcing.publisher.EventSourcingPublisher;
+import org.fireflyframework.eventsourcing.transaction.EventSourcingTransactionalAspect;
+import org.fireflyframework.eventsourcing.upcasting.EventUpcaster;
+import org.fireflyframework.eventsourcing.upcasting.EventUpcastingService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.ComponentScan;
-import org.springframework.context.annotation.FilterType;
-import org.springframework.context.annotation.Import;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.transaction.ReactiveTransactionManager;
+
+import java.util.List;
/**
* Auto-configuration for the Event Sourcing library.
@@ -59,23 +68,8 @@
@AutoConfiguration
@ConditionalOnProperty(prefix = "firefly.eventsourcing", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(EventSourcingProperties.class)
-@ComponentScan(
- basePackages = "org.fireflyframework.eventsourcing",
- excludeFilters = @ComponentScan.Filter(
- type = FilterType.REGEX,
- pattern = "com\\.firefly\\.common\\.eventsourcing\\.examples\\..*"
- )
-)
@EnableAsync
@EnableScheduling
-@Import({
- EventStoreAutoConfiguration.class,
- SnapshotAutoConfiguration.class,
- EventSourcingHealthConfiguration.class,
- EventSourcingMetricsConfiguration.class,
- org.fireflyframework.core.config.R2dbcConfig.class,
- org.fireflyframework.core.config.R2dbcTransactionConfig.class
-})
@Slf4j
public class EventSourcingAutoConfiguration {
@@ -111,4 +105,76 @@ public ObjectMapper eventSourcingObjectMapper() {
mapper.findAndRegisterModules();
return mapper;
}
+
+ /**
+ * Creates the EventStoreMetrics bean for monitoring event store operations.
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ public EventStoreMetrics eventStoreMetrics(MeterRegistry meterRegistry) {
+ log.debug("Creating EventStoreMetrics bean");
+ return new EventStoreMetrics(meterRegistry);
+ }
+
+ /**
+ * Creates the EventTypeRegistry bean for automatic event type discovery and registration.
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ public EventTypeRegistry eventTypeRegistry(ApplicationContext applicationContext, ObjectMapper objectMapper) {
+ log.debug("Creating EventTypeRegistry bean");
+ return new EventTypeRegistry(applicationContext, objectMapper);
+ }
+
+ /**
+ * Creates the EventSourcingTransactionalAspect bean for transactional event sourcing operations.
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnBean(ReactiveTransactionManager.class)
+ public EventSourcingTransactionalAspect eventSourcingTransactionalAspect(
+ ReactiveTransactionManager transactionManager,
+ EventSourcingPublisher eventPublisher) {
+ log.debug("Creating EventSourcingTransactionalAspect bean");
+ return new EventSourcingTransactionalAspect(transactionManager, eventPublisher);
+ }
+
+ /**
+ * Creates the EventUpcastingService bean for managing event upcasting.
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ public EventUpcastingService eventUpcastingService(List
- * This configuration sets up health checks for event stores,
- * snapshot stores, and other event sourcing components.
+ * Registers Spring Boot Actuator health checks for the event store,
+ * outbox, snapshot store, and projections when Actuator is on the classpath.
*/
@Configuration
@ConditionalOnClass(name = "org.springframework.boot.actuator.health.HealthIndicator")
@@ -37,7 +50,39 @@ public EventSourcingHealthConfiguration() {
log.debug("Event Sourcing Health Configuration initialized");
}
- // TODO: Add health indicator bean configurations
- // @Bean
- // public EventStoreHealthIndicator eventStoreHealthIndicator(EventStore eventStore) { ... }
-}
\ No newline at end of file
+ @Bean
+ @ConditionalOnBean(EventStore.class)
+ @ConditionalOnMissingBean(EventStoreHealthIndicator.class)
+ public EventStoreHealthIndicator eventStoreHealthIndicator(EventStore eventStore) {
+ log.debug("Creating EventStoreHealthIndicator bean");
+ return new EventStoreHealthIndicator(eventStore);
+ }
+
+ @Bean
+ @ConditionalOnBean(EventOutboxService.class)
+ @ConditionalOnMissingBean(OutboxHealthIndicator.class)
+ public OutboxHealthIndicator outboxHealthIndicator(EventOutboxService outboxService) {
+ log.debug("Creating OutboxHealthIndicator bean");
+ return new OutboxHealthIndicator(outboxService);
+ }
+
+ @Bean
+ @ConditionalOnBean(SnapshotStore.class)
+ @ConditionalOnMissingBean(SnapshotStoreHealthIndicator.class)
+ public SnapshotStoreHealthIndicator snapshotStoreHealthIndicator(SnapshotStore snapshotStore) {
+ log.debug("Creating SnapshotStoreHealthIndicator bean");
+ return new SnapshotStoreHealthIndicator(snapshotStore);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean(ProjectionHealthIndicator.class)
+ public ProjectionHealthIndicator projectionHealthIndicator(List
- * This configuration sets up Micrometer metrics for event sourcing
- * operations like event appends, reads, snapshot operations, etc.
+ * Wires {@link EventStoreMetrics} to the Micrometer {@link MeterRegistry} when
+ * Micrometer is on the classpath and metrics are enabled.
+ *
+ * Note: The {@link EventStoreMetrics} bean is also defined in
+ * {@link EventSourcingAutoConfiguration}. This configuration class ensures
+ * the metrics bean is available even when loaded through component scanning
+ * independently of the main auto-configuration.
* The event will be automatically discovered and registered when the application starts.
*/
-@Component
@RequiredArgsConstructor
@Slf4j
public class EventTypeRegistry {
diff --git a/src/main/java/org/fireflyframework/eventsourcing/config/SnapshotAutoConfiguration.java b/src/main/java/org/fireflyframework/eventsourcing/config/SnapshotAutoConfiguration.java
index a4fad11..28e938a 100644
--- a/src/main/java/org/fireflyframework/eventsourcing/config/SnapshotAutoConfiguration.java
+++ b/src/main/java/org/fireflyframework/eventsourcing/config/SnapshotAutoConfiguration.java
@@ -16,15 +16,22 @@
package org.fireflyframework.eventsourcing.config;
+import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
+import org.fireflyframework.eventsourcing.snapshot.SnapshotStore;
+import org.fireflyframework.eventsourcing.snapshot.SnapshotTrigger;
+import org.fireflyframework.eventsourcing.snapshot.r2dbc.R2dbcSnapshotStore;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.r2dbc.core.DatabaseClient;
/**
* Auto-configuration for snapshot stores.
*
- * This configuration class sets up snapshot store implementations and
- * related components like caching and cleanup schedulers.
+ * This configuration class sets up the R2DBC-backed snapshot store and a
+ * configurable snapshot trigger that creates snapshots after every N events.
*/
@Configuration
@ConditionalOnProperty(prefix = "firefly.eventsourcing.snapshot", name = "enabled", havingValue = "true", matchIfMissing = true)
@@ -35,8 +42,18 @@ public SnapshotAutoConfiguration() {
log.debug("Snapshot Auto-Configuration initialized");
}
- // TODO: Add snapshot store bean configurations
- // @Bean
- // @ConditionalOnMissingBean
- // public SnapshotStore snapshotStore(...) { ... }
-}
\ No newline at end of file
+ @Bean
+ @ConditionalOnMissingBean
+ public SnapshotStore snapshotStore(DatabaseClient databaseClient, ObjectMapper objectMapper) {
+ log.info("Creating R2dbcSnapshotStore bean");
+ return new R2dbcSnapshotStore(databaseClient, objectMapper);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public SnapshotTrigger snapshotTrigger(SnapshotStore snapshotStore, EventSourcingProperties properties) {
+ int frequency = properties.getSnapshot().getThreshold();
+ log.info("Creating SnapshotTrigger bean with frequency: {} events", frequency);
+ return new SnapshotTrigger(snapshotStore, frequency);
+ }
+}
diff --git a/src/main/java/org/fireflyframework/eventsourcing/examples/ledger/exceptions/AccountClosedException.java b/src/main/java/org/fireflyframework/eventsourcing/examples/ledger/exceptions/AccountClosedException.java
index b1fee14..b708e91 100644
--- a/src/main/java/org/fireflyframework/eventsourcing/examples/ledger/exceptions/AccountClosedException.java
+++ b/src/main/java/org/fireflyframework/eventsourcing/examples/ledger/exceptions/AccountClosedException.java
@@ -16,10 +16,12 @@
package org.fireflyframework.eventsourcing.examples.ledger.exceptions;
+import org.fireflyframework.kernel.exception.FireflyException;
+
/**
* Exception thrown when attempting to perform operations on a closed account.
*/
-public class AccountClosedException extends RuntimeException {
+public class AccountClosedException extends FireflyException {
public AccountClosedException(String message) {
super(message);
diff --git a/src/main/java/org/fireflyframework/eventsourcing/examples/ledger/exceptions/AccountFrozenException.java b/src/main/java/org/fireflyframework/eventsourcing/examples/ledger/exceptions/AccountFrozenException.java
index 2f42172..0e4e69c 100644
--- a/src/main/java/org/fireflyframework/eventsourcing/examples/ledger/exceptions/AccountFrozenException.java
+++ b/src/main/java/org/fireflyframework/eventsourcing/examples/ledger/exceptions/AccountFrozenException.java
@@ -16,10 +16,12 @@
package org.fireflyframework.eventsourcing.examples.ledger.exceptions;
+import org.fireflyframework.kernel.exception.FireflyException;
+
/**
* Exception thrown when attempting to withdraw from a frozen account.
*/
-public class AccountFrozenException extends RuntimeException {
+public class AccountFrozenException extends FireflyException {
public AccountFrozenException(String message) {
super(message);
diff --git a/src/main/java/org/fireflyframework/eventsourcing/examples/ledger/exceptions/InsufficientFundsException.java b/src/main/java/org/fireflyframework/eventsourcing/examples/ledger/exceptions/InsufficientFundsException.java
index 75181dd..8c18d4b 100644
--- a/src/main/java/org/fireflyframework/eventsourcing/examples/ledger/exceptions/InsufficientFundsException.java
+++ b/src/main/java/org/fireflyframework/eventsourcing/examples/ledger/exceptions/InsufficientFundsException.java
@@ -16,10 +16,12 @@
package org.fireflyframework.eventsourcing.examples.ledger.exceptions;
+import org.fireflyframework.kernel.exception.FireflyException;
+
/**
* Exception thrown when attempting to withdraw more than the available balance.
*/
-public class InsufficientFundsException extends RuntimeException {
+public class InsufficientFundsException extends FireflyException {
public InsufficientFundsException(String message) {
super(message);
diff --git a/src/main/java/org/fireflyframework/eventsourcing/examples/ledger/exceptions/InvalidAmountException.java b/src/main/java/org/fireflyframework/eventsourcing/examples/ledger/exceptions/InvalidAmountException.java
index a5993dc..87470e1 100644
--- a/src/main/java/org/fireflyframework/eventsourcing/examples/ledger/exceptions/InvalidAmountException.java
+++ b/src/main/java/org/fireflyframework/eventsourcing/examples/ledger/exceptions/InvalidAmountException.java
@@ -16,10 +16,12 @@
package org.fireflyframework.eventsourcing.examples.ledger.exceptions;
+import org.fireflyframework.kernel.exception.FireflyException;
+
/**
* Exception thrown when an invalid amount is provided (e.g., negative or zero).
*/
-public class InvalidAmountException extends RuntimeException {
+public class InvalidAmountException extends FireflyException {
public InvalidAmountException(String message) {
super(message);
diff --git a/src/main/java/org/fireflyframework/eventsourcing/health/EventStoreHealthIndicator.java b/src/main/java/org/fireflyframework/eventsourcing/health/EventStoreHealthIndicator.java
new file mode 100644
index 0000000..3e544ef
--- /dev/null
+++ b/src/main/java/org/fireflyframework/eventsourcing/health/EventStoreHealthIndicator.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2024-2026 Firefly Software Solutions Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.fireflyframework.eventsourcing.health;
+
+import lombok.extern.slf4j.Slf4j;
+import org.fireflyframework.eventsourcing.store.EventStore;
+import org.springframework.boot.actuate.health.Health;
+import org.springframework.boot.actuate.health.HealthIndicator;
+
+import java.time.Duration;
+
+/**
+ * Health indicator for the event store.
+ * Reports R2DBC connectivity, event count, and latest global sequence.
+ */
+@Slf4j
+public class EventStoreHealthIndicator implements HealthIndicator {
+
+ private final EventStore eventStore;
+ private final Duration timeout;
+
+ public EventStoreHealthIndicator(EventStore eventStore) {
+ this(eventStore, Duration.ofSeconds(5));
+ }
+
+ public EventStoreHealthIndicator(EventStore eventStore, Duration timeout) {
+ this.eventStore = eventStore;
+ this.timeout = timeout;
+ }
+
+ @Override
+ public Health health() {
+ try {
+ Boolean healthy = eventStore.isHealthy()
+ .timeout(timeout)
+ .block();
+
+ if (Boolean.TRUE.equals(healthy)) {
+ Health.Builder builder = Health.up();
+
+ // Attempt to fetch statistics (non-critical)
+ try {
+ var stats = eventStore.getStatistics()
+ .timeout(timeout)
+ .block();
+ if (stats != null) {
+ builder.withDetail("totalEvents", stats.getTotalEvents())
+ .withDetail("totalAggregates", stats.getTotalAggregates())
+ .withDetail("currentGlobalSequence", stats.getCurrentGlobalSequence());
+ }
+ } catch (Exception e) {
+ log.debug("Could not fetch event store statistics for health: {}", e.getMessage());
+ }
+
+ return builder.build();
+ }
+
+ return Health.down()
+ .withDetail("error", "Event store health check returned false")
+ .build();
+ } catch (Exception e) {
+ log.error("Event store health check failed", e);
+ return Health.down()
+ .withDetail("error", "Health check failed: " + e.getMessage())
+ .build();
+ }
+ }
+}
diff --git a/src/main/java/org/fireflyframework/eventsourcing/health/OutboxHealthIndicator.java b/src/main/java/org/fireflyframework/eventsourcing/health/OutboxHealthIndicator.java
new file mode 100644
index 0000000..47d66e4
--- /dev/null
+++ b/src/main/java/org/fireflyframework/eventsourcing/health/OutboxHealthIndicator.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2024-2026 Firefly Software Solutions Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.fireflyframework.eventsourcing.health;
+
+import lombok.extern.slf4j.Slf4j;
+import org.fireflyframework.eventsourcing.outbox.EventOutboxService;
+import org.springframework.boot.actuate.health.Health;
+import org.springframework.boot.actuate.health.HealthIndicator;
+
+import java.time.Duration;
+
+/**
+ * Health indicator for the event outbox.
+ * Reports pending and failed entry counts, with configurable thresholds.
+ */
+@Slf4j
+public class OutboxHealthIndicator implements HealthIndicator {
+
+ private final EventOutboxService outboxService;
+ private final Duration timeout;
+ private final long pendingWarningThreshold;
+ private final long failedDownThreshold;
+
+ public OutboxHealthIndicator(EventOutboxService outboxService) {
+ this(outboxService, Duration.ofSeconds(5), 1000L, 100L);
+ }
+
+ public OutboxHealthIndicator(EventOutboxService outboxService, Duration timeout,
+ long pendingWarningThreshold, long failedDownThreshold) {
+ this.outboxService = outboxService;
+ this.timeout = timeout;
+ this.pendingWarningThreshold = pendingWarningThreshold;
+ this.failedDownThreshold = failedDownThreshold;
+ }
+
+ @Override
+ public Health health() {
+ try {
+ var stats = outboxService.getStatistics()
+ .timeout(timeout)
+ .block();
+
+ if (stats == null) {
+ return Health.unknown()
+ .withDetail("error", "Could not retrieve outbox statistics")
+ .build();
+ }
+
+ Health.Builder builder = Health.up()
+ .withDetail("pendingEntries", stats.pendingCount())
+ .withDetail("failedEntries", stats.failedCount())
+ .withDetail("completedEntries", stats.completedCount())
+ .withDetail("deadLetterEntries", stats.deadLetterCount());
+
+ if (stats.failedCount() > failedDownThreshold) {
+ return builder.down()
+ .withDetail("warning", "Failed entries (" + stats.failedCount()
+ + ") exceed threshold (" + failedDownThreshold + ")")
+ .build();
+ }
+
+ if (stats.pendingCount() > pendingWarningThreshold) {
+ builder.withDetail("warning", "Pending entries (" + stats.pendingCount()
+ + ") exceed threshold (" + pendingWarningThreshold + ")");
+ }
+
+ return builder.build();
+ } catch (Exception e) {
+ log.error("Outbox health check failed", e);
+ return Health.down()
+ .withDetail("error", "Health check failed: " + e.getMessage())
+ .build();
+ }
+ }
+}
diff --git a/src/main/java/org/fireflyframework/eventsourcing/health/SnapshotStoreHealthIndicator.java b/src/main/java/org/fireflyframework/eventsourcing/health/SnapshotStoreHealthIndicator.java
new file mode 100644
index 0000000..5a9db02
--- /dev/null
+++ b/src/main/java/org/fireflyframework/eventsourcing/health/SnapshotStoreHealthIndicator.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2024-2026 Firefly Software Solutions Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.fireflyframework.eventsourcing.health;
+
+import lombok.extern.slf4j.Slf4j;
+import org.fireflyframework.eventsourcing.snapshot.SnapshotStore;
+import org.springframework.boot.actuate.health.Health;
+import org.springframework.boot.actuate.health.HealthIndicator;
+
+import java.time.Duration;
+
+/**
+ * Health indicator for the snapshot store.
+ * Reports connectivity and basic statistics.
+ */
+@Slf4j
+public class SnapshotStoreHealthIndicator implements HealthIndicator {
+
+ private final SnapshotStore snapshotStore;
+ private final Duration timeout;
+
+ public SnapshotStoreHealthIndicator(SnapshotStore snapshotStore) {
+ this(snapshotStore, Duration.ofSeconds(5));
+ }
+
+ public SnapshotStoreHealthIndicator(SnapshotStore snapshotStore, Duration timeout) {
+ this.snapshotStore = snapshotStore;
+ this.timeout = timeout;
+ }
+
+ @Override
+ public Health health() {
+ try {
+ Boolean healthy = snapshotStore.isHealthy()
+ .timeout(timeout)
+ .block();
+
+ if (Boolean.TRUE.equals(healthy)) {
+ Health.Builder builder = Health.up();
+
+ try {
+ var stats = snapshotStore.getStatistics()
+ .timeout(timeout)
+ .block();
+ if (stats != null) {
+ builder.withDetail("totalSnapshots", stats.getTotalSnapshots())
+ .withDetail("totalAggregatesWithSnapshots", stats.getTotalAggregatesWithSnapshots());
+ if (stats.getTotalStorageSizeBytes() != null) {
+ builder.withDetail("totalStorageBytes", stats.getTotalStorageSizeBytes());
+ }
+ }
+ } catch (Exception e) {
+ log.debug("Could not fetch snapshot statistics for health: {}", e.getMessage());
+ }
+
+ return builder.build();
+ }
+
+ return Health.down()
+ .withDetail("error", "Snapshot store health check returned false")
+ .build();
+ } catch (Exception e) {
+ log.error("Snapshot store health check failed", e);
+ return Health.down()
+ .withDetail("error", "Health check failed: " + e.getMessage())
+ .build();
+ }
+ }
+}
diff --git a/src/main/java/org/fireflyframework/eventsourcing/monitoring/EventStoreMetrics.java b/src/main/java/org/fireflyframework/eventsourcing/monitoring/EventStoreMetrics.java
index f1ed16d..b2c60ed 100644
--- a/src/main/java/org/fireflyframework/eventsourcing/monitoring/EventStoreMetrics.java
+++ b/src/main/java/org/fireflyframework/eventsourcing/monitoring/EventStoreMetrics.java
@@ -21,8 +21,6 @@
import lombok.extern.slf4j.Slf4j;
import org.fireflyframework.observability.metrics.FireflyMetricsSupport;
import org.springframework.boot.actuate.health.Health;
-import org.springframework.stereotype.Component;
-
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.atomic.AtomicLong;
@@ -42,7 +40,6 @@
* - firefly.eventsourcing.connection.pool.active (Gauge)
* - firefly.eventsourcing.batch.size (DistributionSummary)
*/
-@Component
@Slf4j
public class EventStoreMetrics extends FireflyMetricsSupport {
diff --git a/src/main/java/org/fireflyframework/eventsourcing/outbox/EventOutboxProcessor.java b/src/main/java/org/fireflyframework/eventsourcing/outbox/EventOutboxProcessor.java
index 66476c4..1878302 100644
--- a/src/main/java/org/fireflyframework/eventsourcing/outbox/EventOutboxProcessor.java
+++ b/src/main/java/org/fireflyframework/eventsourcing/outbox/EventOutboxProcessor.java
@@ -19,9 +19,7 @@
import org.fireflyframework.eventsourcing.logging.EventSourcingLoggingContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
/**
* Background processor for Event Outbox entries.
@@ -50,13 +48,6 @@
* - Quartz for more advanced scheduling
* - Dedicated outbox processor service
*/
-@Component
-@ConditionalOnProperty(
- prefix = "eventsourcing.outbox.processor",
- name = "enabled",
- havingValue = "true",
- matchIfMissing = false
-)
@RequiredArgsConstructor
@Slf4j
public class EventOutboxProcessor {
diff --git a/src/main/java/org/fireflyframework/eventsourcing/outbox/EventOutboxService.java b/src/main/java/org/fireflyframework/eventsourcing/outbox/EventOutboxService.java
index 9278fcf..192eb6f 100644
--- a/src/main/java/org/fireflyframework/eventsourcing/outbox/EventOutboxService.java
+++ b/src/main/java/org/fireflyframework/eventsourcing/outbox/EventOutboxService.java
@@ -26,7 +26,6 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
-import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -53,7 +52,6 @@
* 3. Retries failed publications with exponential backoff
* 4. Marks entries as completed after successful publication
*/
-@Service
@RequiredArgsConstructor
@Slf4j
public class EventOutboxService {
diff --git a/src/main/java/org/fireflyframework/eventsourcing/publisher/EventPublishingException.java b/src/main/java/org/fireflyframework/eventsourcing/publisher/EventPublishingException.java
index ef25883..b0d7666 100644
--- a/src/main/java/org/fireflyframework/eventsourcing/publisher/EventPublishingException.java
+++ b/src/main/java/org/fireflyframework/eventsourcing/publisher/EventPublishingException.java
@@ -16,6 +16,8 @@
package org.fireflyframework.eventsourcing.publisher;
+import org.fireflyframework.kernel.exception.FireflyInfrastructureException;
+
/**
* Exception thrown when event publishing fails.
*
@@ -25,7 +27,7 @@
* - Messaging infrastructure is unavailable
* - Publisher configuration is invalid
*/
-public class EventPublishingException extends RuntimeException {
+public class EventPublishingException extends FireflyInfrastructureException {
public EventPublishingException(String message) {
super(message);
diff --git a/src/main/java/org/fireflyframework/eventsourcing/publisher/EventSourcingPublisher.java b/src/main/java/org/fireflyframework/eventsourcing/publisher/EventSourcingPublisher.java
index a1012e4..aed8ac2 100644
--- a/src/main/java/org/fireflyframework/eventsourcing/publisher/EventSourcingPublisher.java
+++ b/src/main/java/org/fireflyframework/eventsourcing/publisher/EventSourcingPublisher.java
@@ -112,9 +112,13 @@ public Mono
@@ -26,7 +28,7 @@
* - Invalid snapshot data
* - Configuration issues
*/
-public class SnapshotException extends RuntimeException {
+public class SnapshotException extends FireflyInfrastructureException {
public SnapshotException(String message) {
super(message);
diff --git a/src/main/java/org/fireflyframework/eventsourcing/snapshot/SnapshotTrigger.java b/src/main/java/org/fireflyframework/eventsourcing/snapshot/SnapshotTrigger.java
new file mode 100644
index 0000000..5c32bd4
--- /dev/null
+++ b/src/main/java/org/fireflyframework/eventsourcing/snapshot/SnapshotTrigger.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2024-2026 Firefly Software Solutions Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.fireflyframework.eventsourcing.snapshot;
+
+import lombok.extern.slf4j.Slf4j;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
+
+/**
+ * Determines when snapshots should be created and triggers their creation asynchronously.
+ *
+ * After events are appended to an aggregate, the trigger checks whether the new version
+ * crosses the configured frequency threshold (e.g., every 100 events). If so, a snapshot
+ * save is initiated on a bounded-elastic scheduler to avoid blocking the main event-append path. Snapshot creation is fire-and-forget from the caller's perspective — failures are logged
+ * but never propagate to the event-append operation. Uses {@link DatabaseClient} for reactive, non-blocking snapshot persistence
+ * against the {@code snapshots} table created by the V2 migration. Snapshots are serialized as JSON with an embedded class name wrapper so that
+ * concrete {@link Snapshot} subtypes can be deserialized polymorphically without
+ * requiring {@code @JsonTypeInfo} on the Snapshot interface.
@@ -26,7 +28,7 @@
* - Storage capacity issues
* - Configuration problems
*/
-public class EventStoreException extends RuntimeException {
+public class EventStoreException extends FireflyInfrastructureException {
public EventStoreException(String message) {
super(message);
@@ -37,6 +39,6 @@ public EventStoreException(String message, Throwable cause) {
}
public EventStoreException(Throwable cause) {
- super(cause);
+ super(cause.getMessage(), cause);
}
}
\ No newline at end of file
diff --git a/src/main/java/org/fireflyframework/eventsourcing/store/r2dbc/R2dbcEventStore.java b/src/main/java/org/fireflyframework/eventsourcing/store/r2dbc/R2dbcEventStore.java
index 996255c..6029f4a 100644
--- a/src/main/java/org/fireflyframework/eventsourcing/store/r2dbc/R2dbcEventStore.java
+++ b/src/main/java/org/fireflyframework/eventsourcing/store/r2dbc/R2dbcEventStore.java
@@ -348,9 +348,31 @@ public Flux