An open API service indexing awesome lists of open source software.

https://github.com/mahdibohloul/statemachine

Lightweight, reactive state-machine building blocks for Jvm and Spring.
https://github.com/mahdibohloul/statemachine

java kotlin project-reactor reactive-programming spring statemachine

Last synced: about 2 months ago
JSON representation

Lightweight, reactive state-machine building blocks for Jvm and Spring.

Awesome Lists containing this project

README

          

## statemachine

Lightweight, reactive state-machine building blocks for Kotlin and Spring.

This library helps you implement robust, composable state transitions with Project Reactor, providing a clear separation
of concerns across three phases: before, during, and after transformation.

### Why this library?

- **Reactive-first**: Built on Reactor `Mono` for non-blocking flows.
- **Composable**: Chain actions, guards, and error handlers with simple operators.
- **Type-safe**: Generics enforce correct container/state usage.
- **Spring-friendly**: Factories and annotations integrate with Spring's `ApplicationContext`.
- **Transaction-aware**: Execute after-commit actions only when a transaction successfully commits.
- **Production-ready**: Used in production at Tapsi for complex delivery request state management.

### At a glance

```kotlin
@StateMachineState
class OrderProcessingTransformer(
private val containerProvider: OrderContainerProvider,
private val responseProvider: OrderResponseProvider,
private val actionFactory: OnTransformationActionFactory,
private val guardFactory: OnTransformationGuardFactory,
) : StateTransformerAdapter() {

override fun getState(): OrderStatus = OrderStatus.Processing
override fun getOrder(): Int = Ordered.HIGHEST_PRECEDENCE

override fun configure(configurer: StateMachineConfigurer) {
configurer.apply {
sourceState = OrderStatus.Created
targetState = OrderStatus.Processing
transformationContainerProvider = containerProvider
transformationResponseProvider = responseProvider
onTransformationErrorHandler = OrderErrorHandler()
}
}

override fun configure(configurer: BeforeTransformationConfigurer) {
configurer.apply {
beforeTransformationGuard = guardFactory.getGuard(
PaymentValidationGuard::class,
InventoryCheckGuard::class
)
beforeTransformationAction = actionFactory.getAction(
EnrichOrderAction::class,
ReserveInventoryAction::class
)
}
}

override fun configure(configurer: DuringTransformationConfigurer) {
configurer.apply {
duringTransformationAction = actionFactory.getAction(
ProcessPaymentAction::class,
SaveOrderAction::class
)
}
}

override fun configure(configurer: AfterTransformationConfigurer) {
configurer.apply {
afterTransformationAction = actionFactory.getAction(
SendConfirmationAction::class
)
afterCommitTransactionAction = actionFactory.getAction(
NotifyWarehouseAction::class
)
}
}
}
```

---

## Installation

Available as a Maven artifact.

Maven:

```xml

io.github.mahdibohloul
statemachine
0.11.0

```

Minimums:

- Kotlin 1.9+
- Java 21+
- Spring 6.1+/Boot 3.3+ (optional, for Spring integration)
- Reactor 3.7+

---

## Core Concepts

### Basic Types
- **States (`TEnum : Enum<*>`)**: Your domain states (e.g., `Created`, `Processing`, `Completed`).
- **Request (`TransformationRequest`)**: Marker type representing the input to a transformation.
- **Container (`TransformationContainer`)**: Holds the working state and data during a transformation, including
`source` and `target` states.
- **Response**: The final output type returned after successful transformation.

### Behaviors
- **Actions (`OnTransformationAction`)**: Mutate or enrich the container. Can be chained using `andThen`.
- **Guards (`OnTransformationGuard`)**: Validate whether execution should proceed. Return `Mono`.
- **Error Handler (`OnTransformationErrorHandler`)**: Maps errors to a fallback `Mono`.

### Providers
- **Container Provider (`TransformationContainerProvider`)**: Builds the container from the incoming request and desired states.
- **Response Provider (`TransformationResponseProvider`)**: Maps the final container to your response type.

### Configuration Phases
- **Before Transformation**: Validation guards and pre-processing actions
- **During Transformation**: Core domain logic and state changes
- **After Transformation**: Post-processing, notifications, and after-commit hooks

### Factories (Spring Integration)
- **Action Factory (`OnTransformationActionFactory`)**: Composes multiple actions from Spring beans
- **Guard Factory (`OnTransformationGuardFactory`)**: Composes multiple guards from Spring beans
- **Error Handler Factory (`OnTransformationErrorHandlerFactory`)**: Composes multiple error handlers from Spring beans

---

## Quickstart

### 1. Define Your Domain Types

```kotlin
// States
enum class OrderStatus {
Created, Processing, Shipped, Delivered, Cancelled
}

// Request
sealed class OrderRequest : TransformationRequest {
data class Create(val customerId: String, val items: List) : OrderRequest()
data class Cancel(val orderId: String, val reason: String) : OrderRequest()
}

// Container
data class OrderContainer(
val order: Order,
val customer: Customer,
override val source: OrderStatus? = null,
override val target: OrderStatus? = null,
) : TransformationContainer
```

### 2. Implement Actions and Guards

```kotlin
@Component
class ValidatePaymentAction : OnTransformationAction {
override fun execute(container: OrderContainer): Mono =
Mono.fromCallable {
// Validate payment logic
container.copy(order = container.order.copy(paymentValidated = true))
}
}

@Component
class InventoryCheckGuard : OnTransformationGuard {
// Requires: import io.github.mahdibohloul.statemachine.guards.GuardDecision
// import io.github.mahdibohloul.statemachine.StateMachineErrorCodeString
override fun executeDecision(container: OrderContainer): Mono =
Mono.fromCallable {
val allItemsAvailable = container.order.items.all { item -> checkInventory(item) }
if (allItemsAvailable) {
GuardDecision.Allow
} else {
GuardDecision.Deny(
errorCode = StateMachineErrorCodeString.GuardValidationFailed,
cause = InsufficientInventoryException(container.order.items)
)
}
}
}
```

### 3. Create Container and Response Providers

```kotlin
@Component
class OrderContainerProvider : TransformationContainerProvider {
override fun provideContainer(
request: OrderRequest,
source: OrderStatus?,
target: OrderStatus?
): Mono = when (request) {
is OrderRequest.Create -> Mono.fromCallable {
OrderContainer(
order = Order.fromRequest(request),
customer = getCustomer(request.customerId),
source = source,
target = target
)
}
is OrderRequest.Cancel -> Mono.fromCallable {
OrderContainer(
order = getOrder(request.orderId),
customer = getCustomerByOrder(request.orderId),
source = source,
target = target
)
}
}
}

@Component
class OrderResponseProvider : TransformationResponseProvider {
override fun provideResponse(request: OrderRequest, container: OrderContainer): Mono =
Mono.just(container.order)
}
```

### 4. Implement the Transformer

```kotlin
@StateMachineState
class OrderProcessingTransformer(
private val containerProvider: OrderContainerProvider,
private val responseProvider: OrderResponseProvider,
private val actionFactory: OnTransformationActionFactory,
private val guardFactory: OnTransformationGuardFactory,
) : StateTransformerAdapter() {

override fun getState(): OrderStatus = OrderStatus.Processing
override fun getOrder(): Int = Ordered.HIGHEST_PRECEDENCE

override fun configure(configurer: StateMachineConfigurer) {
configurer.apply {
sourceState = OrderStatus.Created
targetState = OrderStatus.Processing
transformationContainerProvider = containerProvider
transformationResponseProvider = responseProvider
onTransformationErrorHandler = OrderErrorHandler()
}
}

override fun configure(configurer: BeforeTransformationConfigurer) {
configurer.apply {
beforeTransformationGuard = guardFactory.getGuard(
InventoryCheckGuard::class,
PaymentValidationGuard::class
)
beforeTransformationAction = actionFactory.getAction(
ValidatePaymentAction::class,
ReserveInventoryAction::class
)
}
}

override fun configure(configurer: DuringTransformationConfigurer) {
configurer.apply {
duringTransformationAction = actionFactory.getAction(
ProcessPaymentAction::class,
SaveOrderAction::class
)
}
}

override fun configure(configurer: AfterTransformationConfigurer) {
configurer.apply {
afterTransformationAction = actionFactory.getAction(
SendConfirmationAction::class
)
afterCommitTransactionAction = actionFactory.getAction(
NotifyWarehouseAction::class
)
}
}
}
```

### 5. Execute the Transformation

```kotlin
@RestController
class OrderController(
private val orderProcessingTransformer: OrderProcessingTransformer
) {

@PostMapping("/orders")
fun createOrder(@RequestBody request: CreateOrderRequest): Mono =
orderProcessingTransformer.transform(
OrderRequest.Create(request.customerId, request.items)
)
}
```

---

## Advanced Usage

### Composing Behaviors

Use `andThen` to chain actions and error handlers:

```kotlin
// Chain multiple actions
val compositeAction = ValidatePaymentAction()
.andThen(ReserveInventoryAction())
.andThen(SendNotificationAction())

// Chain multiple error handlers
val compositeErrorHandler = LogErrorHandler()
.andThen(FallbackResponseHandler())
.andThen(RetryHandler())
```

### Transformer Ordering

Control execution order using `getOrder()`:

```kotlin
object TransformerOrder {
const val HIGH_PRIORITY = Ordered.HIGHEST_PRECEDENCE
const val MEDIUM_PRIORITY = Ordered.HIGHEST_PRECEDENCE + 10
const val LOW_PRIORITY = Ordered.LOWEST_PRECEDENCE
}

class HighPriorityTransformer : StateTransformerAdapter<...>() {
override fun getOrder(): Int = TransformerOrder.HIGH_PRIORITY
}
```

### Custom Validation in canHandle()

```kotlin
abstract class AbstractOrderTransformer :
StateTransformerAdapter() {

override fun canHandle(transformerIdentifier: StateMachineStateFactory.TransformerIdentifier) {
super.canHandle(transformerIdentifier)
val request = transformerIdentifier.getTransformationRequest()

require(isSupportedOrderType(request)) {
"Order type ${request.type} is not supported by this transformer"
}
}

protected abstract fun isSupportedOrderType(request: OrderRequest): Boolean
}
```

### Domain-Specific Error Handling

```kotlin
@Component
class OrderErrorHandler : OnTransformationErrorHandler {
override fun onError(request: OrderRequest, error: Throwable): Mono =
when (error) {
is InsufficientInventoryException ->
Mono.error(OrderException.InventoryUnavailable(error.itemIds))
is PaymentFailedException ->
Mono.error(OrderException.PaymentDeclined(error.reason))
is DuplicateKeyException ->
Mono.error(OrderException.DuplicateOrder(request.orderId))
else -> Mono.error(error)
}
}
```

### Guard Decision API and Thread Safety

The library provides a thread-safe guard validation API through the `GuardDecision` sealed interface. This prevents concurrency issues when multiple threads access stateless guards.

**Why GuardDecision?**

In concurrent environments, stateless guards that rely on instance methods to provide error codes can experience race conditions. The `GuardDecision` API captures error codes and causes in immutable data structures at decision time, ensuring thread-safe error handling.

**Using GuardDecision:**

```kotlin
@Component
class PaymentValidationGuard : OnTransformationGuard {
override fun executeDecision(container: OrderContainer): Mono =
Mono.fromCallable {
val isValid = validatePayment(container.order.paymentMethod)
if (isValid) {
GuardDecision.Allow
} else {
GuardDecision.Deny(
errorCode = CustomErrorCode.PaymentValidationFailed,
cause = PaymentValidationException("Invalid payment method")
)
}
}
}
```

**Migrating from 0.10.x:** If you previously implemented `execute(container): Mono` (and optional `getValidationFailure*` hooks), replace that with `executeDecision(container): Mono` and return `GuardDecision.Allow` or `GuardDecision.Deny(errorCode, cause)`.

**Benefits:**
- **Thread-safe**: Error codes are captured in immutable `GuardDecision.Deny` objects
- **Type-safe**: Sealed interface ensures exhaustive handling
- **Rich error information**: Error codes and causes are explicitly captured

---

## Spring Integration

### Automatic Bean Discovery

Annotate state-specific components with `@StateMachineState`:

```kotlin
@StateMachineState("Processing")
class OrderProcessingTransformer(
// dependencies injected by Spring
) : StateTransformerAdapter<...>()

@StateMachineState("Approved")
class OrderApprovedTransformer(
// dependencies injected by Spring
) : StateTransformerAdapter<...>()
```

### Factory-Based Composition

Use factories to compose multiple behaviors from Spring beans:

```kotlin
@Configuration
class StateMachineConfiguration {

@Bean
fun actionFactory(applicationContext: ApplicationContext) =
OnTransformationActionFactory(applicationContext)

@Bean
fun guardFactory(applicationContext: ApplicationContext) =
OnTransformationGuardFactory(applicationContext)
}

// In your transformer
override fun configure(configurer: BeforeTransformationConfigurer) {
configurer.apply {
// Compose multiple guards from Spring beans
beforeTransformationGuard = guardFactory.getGuard(
PaymentValidationGuard::class,
InventoryCheckGuard::class,
CustomerEligibilityGuard::class
)

// Compose multiple actions from Spring beans
beforeTransformationAction = actionFactory.getAction(
ValidatePaymentAction::class,
ReserveInventoryAction::class,
SendNotificationAction::class
)
}
}
```

### Manual Wiring (Non-Spring)

You can use the library without Spring by manually wiring dependencies:

```kotlin
class ManualOrderTransformer : StateTransformerAdapter() {

override fun configure(configurer: BeforeTransformationConfigurer) {
configurer.apply {
// Manually compose behaviors
beforeTransformationGuard = PaymentValidationGuard()
.andThen(InventoryCheckGuard())
.andThen(CustomerEligibilityGuard())

beforeTransformationAction = ValidatePaymentAction()
.andThen(ReserveInventoryAction())
.andThen(SendNotificationAction())
}
}
}
```

---

## Transactions and After-Commit Hooks

The library provides transaction-aware execution for after-commit actions. When a reactive transaction is active,
`afterCommitTransactionAction` will only execute after the transaction successfully commits.

### Basic Usage

```kotlin
override fun configure(configurer: AfterTransformationConfigurer) {
configurer.apply {
// This runs immediately after the main transformation
afterTransformationAction = actionFactory.getAction(
SendConfirmationAction::class,
UpdateInventoryAction::class
)

// This runs only after transaction commit (if transaction is active)
afterCommitTransactionAction = actionFactory.getAction(
NotifyWarehouseAction::class,
SendExternalNotificationAction::class
)
}
}
```

### Transaction Scenarios

```kotlin
// Scenario 1: No active transaction
// - afterTransformationAction executes
// - afterCommitTransactionAction is skipped

// Scenario 2: Active transaction that commits successfully
// - afterTransformationAction executes
// - Transaction commits
// - afterCommitTransactionAction executes

// Scenario 3: Active transaction that rolls back
// - afterTransformationAction executes
// - Transaction rolls back
// - afterCommitTransactionAction is skipped
```

### Use Cases for After-Commit Actions

- **External API calls**: Only notify external systems after data is persisted
- **Event publishing**: Publish domain events only after successful persistence
- **Audit logging**: Log state changes only after they're committed
- **Cache invalidation**: Update caches only after database changes are committed

### Example: Order Processing with Notifications

```kotlin
override fun configure(configurer: AfterTransformationConfigurer) {
configurer.apply {
// Immediate actions (run before commit)
afterTransformationAction = actionFactory.getAction(
UpdateOrderStatusAction::class,
SendCustomerEmailAction::class
)

// After-commit actions (run only after successful commit)
afterCommitTransactionAction = actionFactory.getAction(
NotifyWarehouseAction::class, // External system notification
PublishOrderCreatedEvent::class, // Event publishing
UpdateAnalyticsAction::class, // Analytics tracking
InvalidateCacheAction::class // Cache invalidation
)
}
}
```

---

## Error Handling

### Global Error Handler

Set a global error handler via `StateMachineConfigurer.onTransformationErrorHandler`:

```kotlin
override fun configure(configurer: StateMachineConfigurer) {
configurer.apply {
onTransformationErrorHandler = object : OnTransformationErrorHandler {
override fun onError(request: OrderRequest, error: Throwable): Mono =
when (error) {
is InsufficientInventoryException ->
Mono.error(OrderException.InventoryUnavailable(error.itemIds))
is PaymentFailedException ->
Mono.error(OrderException.PaymentDeclined(error.reason))
is DuplicateKeyException ->
Mono.error(OrderException.DuplicateOrder(request.orderId))
else -> Mono.error(error)
}
}
}
}
```

### Composed Error Handlers

Use factories to compose multiple error handlers:

```kotlin
override fun configure(configurer: StateMachineConfigurer) {
configurer.apply {
onTransformationErrorHandler = errorHandlerFactory.getErrorHandler(
LogErrorHandler::class,
FallbackResponseHandler::class,
RetryHandler::class
)
}
}
```

### Common Exceptions

- `StateMachineException.SourceAndTargetAreEqualException`: Source and target states are identical
- `StateMachineException.NoContainerProviderConfiguredException`: No container provider configured

### Error Handler Examples

```kotlin
@Component
class LogErrorHandler : OnTransformationErrorHandler {
override fun onError(request: OrderRequest, error: Throwable): Mono =
Mono.fromRunnable {
logger.error("Order transformation failed for request: $request", error)
}.then(Mono.error(error))
}

@Component
class FallbackResponseHandler : OnTransformationErrorHandler {
override fun onError(request: OrderRequest, error: Throwable): Mono =
Mono.just(Order.failed(error.message ?: "Unknown error"))
}

@Component
class RetryHandler : OnTransformationErrorHandler {
override fun onError(request: OrderRequest, error: Throwable): Mono =
if (isRetryableError(error)) {
Mono.error(error) // Let retry mechanism handle it
} else {
Mono.error(error)
}
}
```

---

## Testing

### Unit Testing Actions and Guards

```kotlin
class OrderActionTest {

@Test
fun `should validate payment successfully`() {
// Given
val container = OrderContainer(
order = Order(paymentValidated = false),
customer = Customer(id = "123")
)
val action = ValidatePaymentAction()

// When & Then
action.execute(container)
.`as`(StepVerifier::create)
.expectNextMatches { it.order.paymentValidated }
.verifyComplete()
}

@Test
fun `should fail when payment is invalid`() {
// Given
val container = OrderContainer(
order = Order(paymentValidated = false, paymentMethod = "INVALID"),
customer = Customer(id = "123")
)
val action = ValidatePaymentAction()

// When & Then
action.execute(container)
.`as`(StepVerifier::create)
.verifyError(PaymentValidationException::class.java)
}
}

@Test
fun `should validate guard successfully with GuardDecision`() {
// Given
val container = OrderContainer(
order = Order(items = listOf(Item(id = "item1", quantity = 2))),
customer = Customer(id = "123")
)
val guard = InventoryCheckGuard()

// When & Then
guard.executeDecision(container)
.`as`(StepVerifier::create)
.expectNextMatches { it is GuardDecision.Allow }
.verifyComplete()
}

@Test
fun `should deny guard validation with error code`() {
// Given
val container = OrderContainer(
order = Order(items = listOf(Item(id = "out-of-stock", quantity = 100))),
customer = Customer(id = "123")
)
val guard = InventoryCheckGuard()

// When & Then
guard.executeDecision(container)
.`as`(StepVerifier::create)
.expectNextMatches { decision ->
decision is GuardDecision.Deny &&
decision.errorCode == StateMachineErrorCodeString.GuardValidationFailed
}
.verifyComplete()
}
```

### Testing Composed Behaviors

```kotlin
@Test
fun `should execute multiple actions in sequence`() {
// Given
val container = OrderContainer(order = Order(value = 100))
val action1 = MultiplyByTwoAction()
val action2 = AddTenAction()
val compositeAction = action1.andThen(action2)

// When & Then
compositeAction.execute(container)
.`as`(StepVerifier::create)
.expectNextMatches { it.order.value == 210 } // (100 * 2) + 10
.verifyComplete()
}
```

### Integration Testing Transformers

```kotlin
@SpringBootTest
class OrderTransformerIntegrationTest {

@Autowired
private lateinit var orderProcessingTransformer: OrderProcessingTransformer

@Test
fun `should process order successfully`() {
// Given
val request = OrderRequest.Create(
customerId = "123",
items = listOf(Item(id = "item1", quantity = 2))
)

// When & Then
orderProcessingTransformer.transform(request)
.`as`(StepVerifier::create)
.expectNextMatches { order ->
order.status == OrderStatus.Processing &&
order.paymentValidated &&
order.inventoryReserved
}
.verifyComplete()
}

@Test
fun `should handle insufficient inventory`() {
// Given
val request = OrderRequest.Create(
customerId = "123",
items = listOf(Item(id = "out-of-stock", quantity = 100))
)

// When & Then
orderProcessingTransformer.transform(request)
.`as`(StepVerifier::create)
.verifyError(OrderException.InventoryUnavailable::class.java)
}
}
```

### Testing Error Handlers

```kotlin
@Test
fun `should handle payment failure gracefully`() {
// Given
val request = OrderRequest.Create(customerId = "123", items = emptyList())
val error = PaymentFailedException("Card declined")
val errorHandler = OrderErrorHandler()

// When & Then
errorHandler.onError(request, error)
.`as`(StepVerifier::create)
.verifyError(OrderException.PaymentDeclined::class.java)
}
```

### Mocking Dependencies

```kotlin
@ExtendWith(MockitoExtension::class)
class OrderTransformerTest {

@Mock
private lateinit var orderService: OrderService

@Mock
private lateinit var paymentService: PaymentService

@Test
fun `should save order after processing`() {
// Given
val container = OrderContainer(order = Order(id = "123"))
val action = SaveOrderAction(orderService)

whenever(orderService.save(any())).thenReturn(Mono.just(Order(id = "123")))

// When
action.execute(container)
.`as`(StepVerifier::create)
.expectNext(container)
.verifyComplete()

// Then
verify(orderService).save(container.order)
}
}
```

---

## Best Practices

### 1. State Design
- Use enums for states to ensure type safety
- Keep state names descriptive and domain-specific
- Avoid too many states - consider if some can be combined

### 2. Container Design
- Keep containers immutable when possible
- Include all necessary domain data in the container
- Use sealed classes for different request types

### 3. Action Composition
- Keep actions focused on single responsibilities
- Use `andThen` to compose related actions
- Prefer small, testable actions over large monolithic ones

### 4. Guard Design
- Guards should be pure validation logic
- Implement `executeDecision()` returning `GuardDecision` (`Allow` or `Deny` with error code and cause)
- Use `GuardDecision.Deny` with specific error codes and causes for failed validations
- Return meaningful error codes and exception causes for better error handling
- Consider using domain-specific exception types in `GuardDecision.Deny`

### 5. Error Handling
- Use domain-specific exceptions
- Provide fallback responses when appropriate
- Log errors with sufficient context

### 6. Transaction Management
- Use after-commit actions for external system notifications
- Keep transaction boundaries clear and minimal
- Test both transactional and non-transactional scenarios

### 7. Testing Strategy
- Test each action and guard in isolation
- Use integration tests for full transformation flows
- Mock external dependencies appropriately

### 8. Performance Considerations
- Use reactive patterns throughout
- Avoid blocking operations in actions and guards
- Consider caching for frequently accessed data

---

## Real-World Example: Delivery Request State Machine

This library is used in production at Tapsi for managing delivery request state transitions. The delivery domain includes:

- **States**: `Created`, `Processing`, `Assigned`, `PickedUp`, `Delivered`, `Cancelled`
- **Request Types**: Creation, modification, cancellation
- **Complex Validation**: Payment validation, inventory checks, driver availability
- **External Integrations**: Payment gateways, driver apps, customer notifications
- **Transaction Management**: Database persistence with after-commit notifications

Key patterns from the production implementation:
- Abstract base transformers for common functionality
- Factory-based composition of behaviors
- Comprehensive error handling with domain-specific exceptions
- Transaction-aware notifications to external systems
- Extensive logging and monitoring

---

## Version Matrix

- Kotlin: 1.9.x
- Reactor: 3.7.x
- Spring: 6.1+/Boot 3.3+
- Java: 21

---

## License

MIT License. See `LICENSE` for details.