Реализовать потокобезопасную обработку статусов заказов

16. Реализовать потокобезопасную обработку статусов заказов

Условие задачи:
Необходимо реализовать сервис обработки событий заказов.

Допустимые переходы:

NEW     → CREATED
CREATED → PAID, CANCELLED
PAID    → SHIPPED, CANCELLED

Для каждого заказа должен храниться только один текущий статус. Обработка нескольких событий для одного заказа должна быть потокобезопасной.

Код:

public class OrderProcessor {

    enum OrderStatus {
        NEW,
        CREATED,
        PAID,
        SHIPPED,
        CANCELLED
    }

    record OrderEvent(
            long orderId,
            OrderStatus newStatus
    ) {}

    private final ? orders = ?;

    private static final ? ALLOWED_TRANSITIONS = ?;

    public OrderStatus handleEvent(OrderEvent event) {
    }

    private boolean isAllowed(
            OrderStatus current,
            OrderStatus next
    ) {
    }

    public OrderStatus getStatus(long orderId) {
    }

    public static void main(String[] args) {
    }
}

Спойлеры к решению

Подсказки
💡 Текущий статус заказа можно хранить в ConcurrentHashMap.
💡 Допустимые переходы удобно представить как Map<OrderStatus, Set<OrderStatus>>.
💡 Для атомарного изменения значения по ключу используй compute().
💡 Повторное событие с уже установленным статусом можно обработать идемпотентно.

Решение
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class OrderProcessor {

    enum OrderStatus {
        NEW,
        CREATED,
        PAID,
        SHIPPED,
        CANCELLED
    }

    record OrderEvent(
            long orderId,
            OrderStatus newStatus
    ) {}

    private final ConcurrentMap<Long, OrderStatus> orders =
            new ConcurrentHashMap<>();

    private static final Map<OrderStatus, Set<OrderStatus>>
            ALLOWED_TRANSITIONS = Map.of(
            OrderStatus.NEW,
            Set.of(OrderStatus.CREATED),

            OrderStatus.CREATED,
            Set.of(
                    OrderStatus.PAID,
                    OrderStatus.CANCELLED
            ),

            OrderStatus.PAID,
            Set.of(
                    OrderStatus.SHIPPED,
                    OrderStatus.CANCELLED
            )
    );

    public OrderStatus handleEvent(OrderEvent event) {
        return orders.compute(
                event.orderId(),
                (id, current) -> {
                    OrderStatus currentStatus =
                            current == null
                                    ? OrderStatus.NEW
                                    : current;

                    if (currentStatus == event.newStatus()) {
                        return currentStatus;
                    }

                    if (!isAllowed(
                            currentStatus,
                            event.newStatus()
                    )) {
                        throw new IllegalStateException(
                                "Transition not allowed: "
                                        + currentStatus
                                        + " -> "
                                        + event.newStatus()
                        );
                    }

                    return event.newStatus();
                }
        );
    }

    private boolean isAllowed(
            OrderStatus current,
            OrderStatus next
    ) {
        return ALLOWED_TRANSITIONS
                .getOrDefault(current, Set.of())
                .contains(next);
    }

    public OrderStatus getStatus(long orderId) {
        return orders.getOrDefault(
                orderId,
                OrderStatus.NEW
        );
    }

    public static void main(String[] args) {
        OrderProcessor processor = new OrderProcessor();

        processor.handleEvent(
                new OrderEvent(1, OrderStatus.CREATED)
        );

        processor.handleEvent(
                new OrderEvent(1, OrderStatus.PAID)
        );

        System.out.println(
                processor.getStatus(1)
        );
    }
}

Результат:

PAID

ALLOWED_TRANSITIONS описывает разрешённые переходы. Например:

OrderStatus.CREATED,
Set.of(
        OrderStatus.PAID,
        OrderStatus.CANCELLED
)

разрешает переход из CREATED только в PAID или CANCELLED.

Ключевой момент — использование:

orders.compute(...)

Проверка текущего состояния и установка нового выполняются атомарно для конкретного orderId.

Поэтому два параллельных события не смогут одновременно изменить один и тот же заказ на основе устаревшего статуса.

Повторное событие:

PAID → PAID

обрабатывается идемпотентно и не изменяет состояние.

Попытка недопустимого перехода, например:

NEW → SHIPPED

приведёт к IllegalStateException.