Skip to content

ADR-001: Product Assignment State Machine

Date Author Status
2026-03-18 Fabian Beyerlein Proposed

Context

The entities of our internal shop system (OrderGroup and OrderRequest) are used to drive the state of an ESL. This tightly couples our internal shop system to the state of an ESL which makes it difficult to add third-party integrations that are supposed to bypass our shop.

Decision

Create a dedicated entity ProductAssignmentState with this pseudo shape

type ProductAssignmentState struct {
  ID           uuid.UUID
  AssignmentID uuid.UUID
  State        AssignmentState
  UpdatedAt    time.Time
  // These audit fields don't really have any constraints
  Reason    string
  UpdatedBy string // integration identifier (internal, erp-x-v1, ...)
}

type AssignmentState string

const (
  AssignmentStateDefault   AssignmentState = "DEFAULT"
  AssignmentStateRequested AssignmentState = "REQUESTED"
)

A port will be provided by the topology module that allows other modules to drive the state of a ProductAssignment. When gathering data for ESL change handling we should fetch the ProductAssignmentState instead of checking for open/unfulfilled OrderRequests. This shifts the responsibility of the state and the logic to derive it to the appropriate party - the shop integration.

There will be no history of this state for now.

Creating a ProductAssignment now also has to create a ProductAssignmentState with DEFAULT as the state.

State Transitions

State transitions are unrestricted for now.

uml diagram

Port

Note

The port uses ProductAssignmentState rather than AssignmentState to avoid naming collisions in the shared ports package. Internally the topology module maps this to its own domain.AssignmentState type.

// backend/apps/solaris/ports/topology.go
package ports

type ProductAssignmentStateManager interface {
  Transition(
    ctx context.Context, orgID, assignmentID uuid.UUID,
    newState ProductAssignmentState, reason, updatedBy string,
  ) error
}

type ProductAssignmentState string

const (
  ProductAssignmentStateDefault   ProductAssignmentState = "DEFAULT"
  ProductAssignmentStateRequested ProductAssignmentState = "REQUESTED"
)

Alternatives considered

  • Keep inferring state from OrderRequest
    • Means for third-party integrations our shop system is always involved, even if nobody uses it
    • Since nobody is using our shop in such cases, we would need workarounds for approving/rejecting OrderRequests to trigger the state change
  • Simple field on ProductAssignment
    • Less extensible
    • Adding audit fields for this means we pollute the ProductAssignment entity with fields like StateReason, StateUpdatedBy

Consequences

  • Allows third-party integrations and other modules to drive the state of an ESL
  • Removes the need to have an OrderRequest to drive state
  • Internal shop system gets refactored to drive state in addition to managing it's own entities
  • Big refactoring burden for the internal "shop system"
  • Migration is going to be difficult, current state has to be inferred from the state of any OrderRequests associated with every ProductAssignment.