Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/microservices-patterns/ftgo-application/llms.txt

Use this file to discover all available pages before exploring further.

This page explains how the FTGO application uses the API Gateway pattern through two separate gateway implementations: a REST-based gateway built with Spring Cloud Gateway and Spring WebFlux (ftgo-api-gateway), and a GraphQL gateway built with Node.js (ftgo-api-gateway-graphql). Both act as the single entry point for external clients, hiding the internal service topology.

Why an API Gateway

Without a gateway, a mobile or web client must know the address of every downstream service, make multiple round trips to assemble a single screen’s worth of data, and handle partial failures from each service independently. An API gateway solves this by:
  • Routing simple requests directly to the owning service.
  • Composing responses that require data from several services into one reply.
  • Abstracting the internal service topology from external clients.

REST Gateway — ftgo-api-gateway

The REST gateway runs on port 8087 and is implemented as a Spring Boot application using Spring Cloud Gateway for routing and Spring WebFlux for reactive API composition.

Routing Configuration

OrderConfiguration registers Spring Cloud Gateway routes for write operations and the order-history list query, plus a WebFlux RouterFunction for the composed order-detail endpoint:
// OrderConfiguration.java
@Configuration
@EnableConfigurationProperties(OrderDestinations.class)
public class OrderConfiguration {

  @Bean
  public RouteLocator orderProxyRouting(RouteLocatorBuilder builder,
                                        OrderDestinations orderDestinations) {
    return builder.routes()
        // POST/PUT /orders  → Order Service
        .route(r -> r.path("/orders").and().method("POST")
                     .uri(orderDestinations.getOrderServiceUrl()))
        .route(r -> r.path("/orders").and().method("PUT")
                     .uri(orderDestinations.getOrderServiceUrl()))
        .route(r -> r.path("/orders/**").and().method("POST")
                     .uri(orderDestinations.getOrderServiceUrl()))
        .route(r -> r.path("/orders/**").and().method("PUT")
                     .uri(orderDestinations.getOrderServiceUrl()))
        // GET /orders  → Order History Service (CQRS read model)
        .route(r -> r.path("/orders").and().method("GET")
                     .uri(orderDestinations.getOrderHistoryServiceUrl()))
        .build();
  }

  @Bean
  public RouterFunction<ServerResponse> orderHandlerRouting(
      OrderHandlers orderHandlers) {
    // GET /orders/{orderId} → composed response
    return RouterFunctions.route(
        GET("/orders/{orderId}"),
        orderHandlers::getOrderDetails);
  }
}
Write requests (POST, PUT) are forwarded transparently to the Order Service. The list query (GET /orders) is forwarded to the Order History Service. Only the single-order detail endpoint (GET /orders/{orderId}) performs API composition.

API Composition for Order Details

OrderHandlers.getOrderDetails calls four downstream services in parallel using Reactor’s Mono.zip, then merges the results into a single OrderDetails response. Services that are unavailable contribute an empty Optional rather than failing the whole request.
// OrderHandlers.java
public Mono<ServerResponse> getOrderDetails(ServerRequest serverRequest) {
  String orderId = serverRequest.pathVariable("orderId");

  Mono<OrderInfo> orderInfo =
      orderService.findOrderById(orderId);

  Mono<Optional<TicketInfo>> ticketInfo =
      kitchenService.findTicketById(orderId)
          .map(Optional::of)
          .onErrorReturn(Optional.empty());

  Mono<Optional<DeliveryInfo>> deliveryInfo =
      deliveryService.findDeliveryByOrderId(orderId)
          .map(Optional::of)
          .onErrorReturn(Optional.empty());

  Mono<Optional<BillInfo>> billInfo =
      accountingService.findBillByOrderId(orderId)
          .map(Optional::of)
          .onErrorReturn(Optional.empty());

  Mono<Tuple4<OrderInfo,
              Optional<TicketInfo>,
              Optional<DeliveryInfo>,
              Optional<BillInfo>>> combined =
      Mono.zip(orderInfo, ticketInfo, deliveryInfo, billInfo);

  Mono<OrderDetails> orderDetails =
      combined.map(OrderDetails::makeOrderDetails);

  return orderDetails
      .flatMap(od -> ServerResponse.ok()
          .contentType(MediaType.APPLICATION_JSON)
          .body(fromObject(od)))
      .onErrorResume(OrderNotFoundException.class,
          e -> ServerResponse.notFound().build());
}
The four proxies (OrderServiceProxy, KitchenService, DeliveryService, AccountingService) all use WebClient to make non-blocking HTTP calls to their respective services.

Services Composed for a Single Order

DataSource Service
OrderInfo (status, line items)Order Service
TicketInfo (kitchen ticket state)Kitchen Service
DeliveryInfo (courier, ETA)Delivery Service
BillInfo (authorization state)Accounting Service
OrderInfo is required — an OrderNotFoundException returns HTTP 404. The other three are optional; a downstream failure yields an empty field rather than a 500 error.

GraphQL Gateway — ftgo-api-gateway-graphql

The GraphQL gateway is a Node.js service using graphql-tools and express-graphql. It provides a typed schema that lets clients fetch exactly the fields they need and traverse relationships (e.g., fetch an order and its consumer in one request).

Schema

// ftgo-api-gateway-graphql/src/schema.js
const typeDefs = gql`
  type Query {
    orders(consumerId: Int!): [Order]
    order(orderId: Int!): Order
    consumer(consumerId: Int!): Consumer
  }

  type Mutation {
    createConsumer(c: ConsumerInfo): Consumer
  }

  type Order {
    orderId: ID
    consumerId: Int
    consumer: Consumer
    restaurant: Restaurant
    deliveryInfo: DeliveryInfo
  }

  type Restaurant {
    id: ID
    name: String
  }

  type Consumer {
    id: ID
    firstName: String
    lastName: String
    orders: [Order]
  }

  input ConsumerInfo {
    firstName: String
    lastName: String
  }

  type DeliveryInfo {
    status: DeliveryStatus
    estimatedDeliveryTime: Int
    assignedCourier: String
  }

  enum DeliveryStatus {
    PREPARING
    READY_FOR_PICKUP
    PICKED_UP
    DELIVERED
  }
`;

Resolvers

Each GraphQL field that requires a downstream call has its own resolver. Related objects are fetched lazily — the consumer field on Order only triggers a Consumer Service call if the client asks for it.
// Root query resolvers
function resolveOrders(_, { consumerId }, context) {
  return context.orderServiceProxy.findOrders(consumerId);
}

function resolveOrder(_, { orderId }, context) {
  return context.orderServiceProxy.findOrder(orderId);
}

function resolveConsumer(_, { consumerId }, context) {
  return context.consumerServiceProxy.findConsumer(consumerId);
}

// Field resolvers on Order
function resolveOrderConsumer({ consumerId }, args, context) {
  return context.consumerServiceProxy.findConsumer(consumerId);
}

function resolveOrderRestaurant({ restaurantId }, args, context) {
  return context.restaurantServiceProxy.findRestaurant(restaurantId);
}

function resolveOrderDeliveryInfo({ orderId }, args, context) {
  return context.deliveryServiceProxy.findDeliveryForOrder(orderId);
}

// Field resolver on Consumer — reuse pre-fetched orders if available
function resolveConsumerOrders({ id, orders }, args, context) {
  return orders || context.orderServiceProxy.findOrders(id);
}

const resolvers = {
  Query:    { orders: resolveOrders, consumer: resolveConsumer, order: resolveOrder },
  Mutation: { createConsumer: createConsumer },
  Order:    { consumer: resolveOrderConsumer, restaurant: resolveOrderRestaurant,
              deliveryInfo: resolveOrderDeliveryInfo },
  Consumer: { orders: resolveConsumerOrders },
};

Comparison: REST vs GraphQL Gateway

AspectREST Gateway (port 8087)GraphQL Gateway
ProtocolHTTP/RESTGraphQL over HTTP
ImplementationJava, Spring Boot, WebFluxNode.js, graphql-tools
CompositionFixed — always fetches from 4 servicesFlexible — client selects fields
Partial failureOptional.empty() for unavailable servicesResolver returns null for unavailable fields
RoutingSpring Cloud Gateway proxy rulesGraphQL resolver dispatch
MutationsForwarded via proxy routescreateConsumer mutation supported
Both gateways proxy to the same downstream services. The REST gateway is suitable for traditional REST consumers; the GraphQL gateway allows richer queries such as fetching a consumer together with their full order history in a single request.

Build docs developers (and LLMs) love