Implementing Finite State Machines in Go for Payment Systems

Finite State Machine

What is a Finite State Machine?

A Finite State Machine (FSM) is a mathematical model of computation that describes a system that can be in exactly one of a finite number of states at any given time. FSMs are particularly useful in scenarios where a system transitions between well-defined states based on specific events or conditions.

Why Use FSM in Payment Systems?

Payment systems, especially in Payment, involve complex state transitions that need to be managed reliably. Here's why FSM is valuable:

  1. Clear State Management: Track payment status clearly (initiated, pending, completed, failed)
  2. Predictable Transitions: Define allowed state changes explicitly
  3. Error Prevention: Prevent invalid state transitions
  4. Audit Trail: Easy tracking of payment flow history
  5. Business Rule Enforcement: Implement payment rules as state transitions

Implementation in Go

Here's a practical implementation of an FSM for a payment payment system:

package payment

import (
    "errors"
    "time"
)

type PaymentState string

const (
    StateInitiated   PaymentState = "INITIATED"
    StatePending     PaymentState = "PENDING"
    StateProcessing  PaymentState = "PROCESSING"
    StateCompleted   PaymentState = "COMPLETED"
    StateFailed      PaymentState = "FAILED"
    StateRefunded    PaymentState = "REFUNDED"
)

type PaymentEvent string

const (
    EventSubmit     PaymentEvent = "SUBMIT"
    EventProcess    PaymentEvent = "PROCESS"
    EventComplete   PaymentEvent = "COMPLETE"
    EventFail       PaymentEvent = "FAIL"
    EventRefund     PaymentEvent = "REFUND"
)

type Payment struct {
    ID            string
    Amount        float64
    CurrentState  PaymentState
    CreatedAt     time.Time
    UpdatedAt     time.Time
}

type StateMachine struct {
    transitions map[PaymentState]map[PaymentEvent]PaymentState
}

func NewPaymentFSM() *StateMachine {
    fsm := &StateMachine{
        transitions: make(map[PaymentState]map[PaymentEvent]PaymentState),
    }

    // Define valid transitions
    fsm.addTransition(StateInitiated, EventSubmit, StatePending)
    fsm.addTransition(StatePending, EventProcess, StateProcessing)
    fsm.addTransition(StateProcessing, EventComplete, StateCompleted)
    fsm.addTransition(StateProcessing, EventFail, StateFailed)
    fsm.addTransition(StateCompleted, EventRefund, StateRefunded)
    fsm.addTransition(StateFailed, EventSubmit, StatePending)

    return fsm
}

// addTransition adds a new transition to the state machine. It associates a given
// payment state and event with a new target state. If the source state does not
// yet have any transitions defined, a new map is created to hold the transitions.
func (fsm *StateMachine) addTransition(from PaymentState, event PaymentEvent, to PaymentState) {
    if fsm.transitions[from] == nil {
        fsm.transitions[from] = make(map[PaymentEvent]PaymentState)
    }
    fsm.transitions[from][event] = to
}

// SendEvent processes a payment event and transitions the payment to a new state.
// If the event is valid for the payment's current state, the payment's CurrentState
// is updated and the UpdatedAt field is set to the current time. If the event is
// invalid for the current state, an error is returned.
func (fsm *StateMachine) SendEvent(payment *Payment, event PaymentEvent) error {
    if transitions, ok := fsm.transitions[payment.CurrentState]; ok {
        if newState, ok := transitions[event]; ok {
            payment.CurrentState = newState
            payment.UpdatedAt = time.Now()
            return nil
        }
    }
    return errors.New("invalid transition")
}

Usage Example

Here's how to use the payment FSM in a real application:

func main() {
    // Create new payment FSM
    fsm := payment.NewPaymentFSM()

	// Create a new payment
	pay := &payment.Payment{
		ID:           "PAY123",
		Amount:       1000.00,
		CurrentState: payment.StateInitiated,
		CreatedAt:    time.Now(),
		UpdatedAt:    time.Now(),
	}

	// Process payment through various states
	err := fsm.SendEvent(pay, payment.EventSubmit)
	if err != nil {
		log.Printf("Error transitioning payment: %v", err)
		return
	}

	// Payment is now in PENDING state
	fmt.Printf("Payment state: %s\n", pay.CurrentState)

	// Continue processing
	err = fsm.SendEvent(pay, payment.EventProcess)
    // ... handle more transitions
}

Payment Flow

In a payment scenario, the FSM helps manage these typical states:

  1. INITIATED

    • Customer initiates payment
    • System generates payment instructions
  2. PENDING

    • Waiting for customer to complete payment
    • Monitoring for incoming payment
  3. PROCESSING

    • Payment detected
    • Verifying payment details
    • Matching with order
  4. COMPLETED

    • Payment verified
    • Order fulfilled
    • Notification sent
  5. FAILED

    • Payment timeout
    • Verification failed
    • Wrong amount transferred

Best Practices

  1. Persistence: Store state transitions in database
  2. Logging: Log all state changes for audit
  3. Timeouts: Implement timeout transitions
  4. Idempotency: Handle duplicate events safely
  5. Recovery: Plan for system failures
  6. Monitoring: Track state distribution metrics

Conclusion

FSMs provide a robust framework for managing payment flows in Go applications. They ensure consistency, maintainability, and reliability in payment processing systems. While the implementation above is simplified, it demonstrates the core concepts needed to build production-ready payment state management.

This is only a basic example. In real-world applications, you'd likely have more complex state transitions, error handling, and additional business rules. The key is to design the FSM to match the specific requirements of your system.

You can use this fsm package for more complex state machines.