WL
Software Engineer
Wassim Lagnaoui

Tutorial 08: Orders API: Part 2

Kitchen queue, serving items and orders, filtering by status, and checking order completion.

← Back to Tutorials

After an order is placed, the kitchen needs to see it and mark items as they go out. This tutorial covers the serving side of the Orders API: the kitchen queue, individual item serving, whole-order status updates, and how to filter served vs. unserved items per session.


1. The kitchen queue

The kitchen screen calls GET /orders/kitchen/queue to see every unserved item across all active orders, sorted by the time the order was placed.

// Repository query: ordered by orderDate so older items appear first
@Query("select o from OrderItem o where o.served = false order by o.order.orderDate")
List<OrderItem> findUnservedOrderItem();

// Service
public List<KitchenOrderQueueResponse> getNotServedItems() {
    List<OrderItem> unservedItems = orderItemRepository.findUnservedOrderItem();

    return unservedItems.stream().map(oi -> {
        KitchenOrderQueueResponse r = new KitchenOrderQueueResponse();
        r.setOrderItemId(oi.getId());
        r.setOrderId(oi.getOrder().getId());
        r.setItemName(oi.getMenuItem().getName());
        r.setTableNumber(oi.getOrder().getTableSession().getTableNumber()); // navigate the chain
        r.setQuantity(oi.getQuantity());
        r.setServed(oi.getServed());
        return r;
    }).collect(Collectors.toList());
}
  • The JPQL query filters at the DB level: no in-memory filtering of all rows.
  • order by o.order.orderDate: JPQL lets you navigate relationships directly in the ORDER BY clause, so items from earlier orders surface first.
  • The table number is reached by navigating orderItem → order → tableSession → tableNumber. Hibernate resolves these joins automatically.

2. Serving an individual item

The kitchen can mark one dish as served without affecting the rest of the order. This allows partial serving as dishes come off the line.

// POST /orders/orderItem/{id}/serve
public MarkOrderItemServedDTO serveOrderItem(Long orderItemId) {
    OrderItem orderItem = orderItemRepository.findById(orderItemId)
        .orElseThrow(() -> new RuntimeException("OrderItem not found"));

    orderItem.setServed(true);
    orderItemRepository.save(orderItem);

    MarkOrderItemServedDTO response = new MarkOrderItemServedDTO();
    response.setOrderItemID(orderItem.getId());
    response.setMessage("Order item marked as served successfully");
    return response;
}
  • Only served is changed: no status string to update, no cascading effect on the parent order.
  • A minimal response (ID + message) is enough here. The kitchen screen just needs confirmation.

3. Marking the whole order as served

// POST /orders/{id}/serve
public OrderResponse markOrderAsServed(Long id) {
    Order order = orderRepository.findById(id)
        .orElseThrow(OrderNotFoundException::new);

    order.setStatus(OrderStatus.SERVED.name());
    orderRepository.save(order);

    return OrderMapper.fromOrder(order);
}
  • This updates the Order.status field to "SERVED": a coarse-grained flag for the whole order, separate from the per-item served boolean.
  • OrderMapper.fromOrder is reused here: a static utility that converts an Order entity to OrderResponse. Because this mapping is used in multiple places, it justifies a dedicated mapper class.

4. Filtering served and unserved items per session

A waiter can see which items for their table are still outstanding. The same stream pattern is used for both endpoints, just with a different filter predicate.

// GET /orders/sessions/{id}/unserved
public List<OrderItemResponse> getUnServedItemsBySession(Long sessionId) {
    tableSessionRepository.findById(sessionId)
        .orElseThrow(TableSessionNotFound::new);

    List<Order> orders = orderRepository.findByTableSession(sessionId);

    return orders.stream()
        .flatMap(order -> order.getItems().stream())       // flatten orders → items
        .filter(oi -> oi.getServed().equals(false))         // only unserved
        .map(oi -> {
            OrderItemResponse r = new OrderItemResponse();
            r.setItemId(oi.getId());
            r.setName(oi.getMenuItem().getName());
            r.setQuantity(oi.getQuantity());
            r.setServed(false);
            r.setTotalPrice(oi.getMenuItem().getPrice() * oi.getQuantity());
            return r;
        })
        .collect(Collectors.toUnmodifiableList());
}

// GET /orders/sessions/{id}/served: same but filter is:
.filter(OrderItem::getServed)
  • flatMap collapses the two-level structure (session has orders, orders have items) into a single flat stream of OrderItems.
  • The served endpoint uses the method reference OrderItem::getServed instead of a lambda: equivalent to oi -> oi.getServed() but cleaner.
  • Collectors.toUnmodifiableList() returns an immutable list. Use Collectors.toList() if the caller needs to mutate it.

5. Checking order completion status

Before a waiter calls the bill, the front end can query whether all items in an order have been served.

// GET /orders/{id}/Items-status
public OrderServedStatusDTO checkStatusOfItemsByOrder(Long id) {
    Order order = orderRepository.findById(id)
        .orElseThrow(OrderNotFoundException::new);

    boolean allServed  = order.getItems().stream().allMatch(OrderItem::getServed);
    boolean someServed = order.getItems().stream().anyMatch(OrderItem::getServed);
    boolean noneServed = order.getItems().stream().noneMatch(OrderItem::getServed);

    OrderServedStatusDTO dto = new OrderServedStatusDTO();
    dto.setOrderId(order.getId());
    dto.setAllItemsServed(allServed);
    dto.setSomeServed(someServed);
    dto.setNoneServed(noneServed);
    return dto;
}
  • allMatch / anyMatch / noneMatch: terminal stream operations that short-circuit: they stop iterating as soon as the answer is known.
  • Returning all three flags in one response lets the front end drive different UI states (e.g., "all ready", "partially ready", "none ready") without three round trips.
  • These are mutually exclusive: if allServed is true, someServed is also true but noneServed is false. The client can check allServed first.

Key Takeaways

  • Filter at the database level with JPQL (where o.served = false), not in Java: it is always faster than fetching all rows and filtering in memory.
  • flatMap is the idiomatic way to flatten a two-level collection (orders → items) into a single stream for filtering and mapping.
  • Two granularities of "served": a per-item boolean served for individual dish tracking, and an order-level status string for coarse workflow state.
  • allMatch / anyMatch / noneMatch short-circuit: prefer them over manual loops when checking predicates across a collection.
  • Extract a mapper class (OrderMapper) when the same entity-to-DTO conversion is needed in more than one place; inline the mapping when it is one-off.