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.