RoboShop AI is a hands-on platform showcase I am building: a robot store with a storefront, checkout, an AI shopping assistant and a Model Context Protocol (MCP) server, packaged as separate services and run on a small Kubernetes lab. It is a learning project. Payments default to Razorpay test mode, the catalogue mixes demo and reference listings, and nothing here represents real customers or production traffic.

This article explains how the application is divided, how three kinds of request travel through it, what the microservices architecture buys, and what it costs. Project-specific claims link to the repository. Where something is an architectural possibility rather than demonstrated behaviour, I say "this design enables".

From Robot Shop to RoboShop AI

RoboShop AI started from Instana's Robot Shop, a polyglot sample application built for learning container orchestration and monitoring. Several boundaries survive: catalogue, user, cart, shipping and payment. The storefront still ships some of the original image assets under their Apache 2.0 licence.

The implementations are mostly new. Shipping keeps the legacy Java design but now runs on Java 21 and Spring Boot over PostgreSQL. The dummy payment service became a Razorpay integration. The AI gateway, the Moks agent, the MCP server, a currency service and a Kafka bridge were added.

What makes a service boundary useful

A boundary earns its cost when it matches a capability that changes for its own reasons, owns its data, and is reached through a narrow contract. It works against you when two services must always change together, share tables, or call each other on every request.

Not every box in a diagram is a business microservice. Traefik and NGINX are the edge. MongoDB, Redis and PostgreSQL are data stores. RabbitMQ and Kafka are brokers. Counting them as services inflates the architecture without adding business capability.

How RoboShop AI divides responsibilities

RoboShop AI service map. Browser traffic enters through Traefik and the web NGINX service, which sends /api requests to ai-gateway and /moks requests to the optional Moks agent. A private mcp-server serves MCP clients through the gateway. The gateway calls user, catalogue, cart, payment, shipping and currency services, each labelled with its data store. Paid orders flow from the gateway outbox through RabbitMQ to shipping-service and kafka-bridge.

Figure 1. Service map: one public origin, the gateway as the API entry point, and each service's data store. Dashed borders mark optional components.
ServiceResponsibilityDependencies / data ownershipDeployment unit
webStatic TypeScript storefront plus NGINX reverse proxyProxies /api/*, /moks/* and /api/currency/rates; no dataDeployment web
ai-gateway (Python, FastAPI)Public API, session checks, admin roles, checkout state machine, event outboxMongoDB settings, chat history and product edits; JSON order ledger on a volume; calls all domain servicesDeployment, 1 replica, Recreate
user-service (Python)Accounts, sessions, MFA, password recovery, purchase historyMongoDB user collections; Amazon SES for emailDeployment
catalogue-service (Node.js)Product reads, search, keyword recommendations, explicit importsReads MongoDB productsDeployment
cart-service (Python)Cart lines with compare-and-set revisionsRedisDeployment
payment-service (Node.js)Razorpay order creation, signature and capture checksRazorpay API; no databaseDeployment
shipping-service (Java)Locations, delivery quotes, shipment requestsDedicated PostgreSQL; consumes RabbitMQDeployment, plus a PostgreSQL Deployment
currency-service (Go)Display exchange rates from the ECB feedIn-memory cache only; optionalDeployment
moks (Python, LangGraph)Shopping-assistant widget and agent enginecatalogue-service; Bedrock or Ollama; no database credentialsDeployment, optional
mcp-server (Python, FastMCP)MCP tools for search, compare and cartCalls the gateway with the caller's session; no dataDeployment, ClusterIP and NetworkPolicy
kafka-bridge (Python)Copies order events from RabbitMQ to KafkaRabbitMQ queue, Kafka topicsContainer in the kafka StatefulSet

The frontend and NGINX are already consolidated. The web README describes one image replacing the former frontend and nginx workloads, and the ingress manifest now targets web.

Data ownership is not perfectly clean. The MongoDB "stores" are separate collections in one instance with a shared application credential. catalogue-service reads products, while the gateway writes admin edits to the same collection. Ownership here is a convention enforced by code paths, not by separate databases.

How an ordinary product request travels

A product page is a static file. Traefik terminates TLS, strips the /roboshop prefix and forwards the request to web, which serves the generated HTML.

The page script then calls /api/products. NGINX proxies it to the gateway without retries or buffering, adds an X-Request-ID and forwards the W3C traceparent header. The gateway calls catalogue-service, which reads MongoDB.

Before responding, the gateway recalculates instock from reservations and sales in its own ledger. Stock authority lives with checkout, not the catalogue. A product list therefore touches two services and a database. The browser fetches currency rates separately; if that fails, original prices stay visible.

How an AI shopping request differs

AI request paths. Path A: browser widget, web, Moks widget routes, ai-gateway /api/chat, Moks engine, which reads catalogue-service, confirms its configuration revision with the gateway and calls Bedrock or Ollama. Path B: MCP client, private mcp-server, ai-gateway, domain services. Moks does not call MCP.

Figure 2. The assistant and MCP are independent entry points. Numbers follow one chat turn.

The chat widget posts to /moks/chat, and NGINX blocks direct browser calls to /api/chat. Moks's widget routes check the session and Origin, then call the gateway. The gateway applies an AI rate limit and allows two concurrent chats; extra requests get 429 assistant-busy.

The gateway adds saved product references, the SKUs that cannot be shown, and the active model configuration with its revision. It then calls the Moks engine with a service token.

The engine reads the catalogue from catalogue-service and filters evidence by stock, budget and currency, keeping at most four products. It runs a short LangGraph graph: search_catalogue → ground_evidence → generate_answer. Before the model call it confirms the configuration revision with the gateway.

Only Amazon Bedrock or an approved Ollama endpoint can generate the answer, and a fresh install has no model connected. The gateway re-applies live stock and saves the turn. See the Moks README for the full flow.

Compared with a product request, this path has more hops and explicit budgets:

  • a 16-second agent deadline;
  • two concurrent provider calls per process;
  • a circuit breaker that opens after three failures.

Moks has no database credentials and no tools that change carts or orders.

The MCP server is a separate entry point for MCP clients. It accepts the caller's own RoboShop session and forwards it to the gateway, which re-authorizes every call. Ten read tools and update_cart are registered; admin write tools require an environment flag and a signed confirmation.

The MCP Service is ClusterIP-only, and its NetworkPolicy admits only labelled pods. No workload carries that label today, so access is through local stdio or a port-forward. Moks never calls MCP: the gateway already calls Moks, so routing Moks through MCP would create a loop.

How checkout and background events work

Checkout and paid-order events. The browser creates an order at ai-gateway with an idempotency key; the gateway reserves stock in its ledger and calls payment-service and Razorpay, which can also send a signed webhook. Once paid, the ledger's outbox publishes to RabbitMQ: live orders go to shipping-service and PostgreSQL, with a dead-letter queue after five failures; test orders go to an audit queue; both go through kafka-bridge to Kafka.

Figure 3. Money moves synchronously; fulfilment and analytics follow asynchronously.

Checkout is synchronous and deliberately conservative. The browser creates an order with an Idempotency-Key. The gateway snapshots the cart, reserves stock and records the order as creating before asking payment-service to create a Razorpay order.

After payment, the server checks Razorpay's HMAC signature and fetches the payment to confirm it was captured with the expected amount and currency. A signed webhook recovers callbacks the browser missed. If the provider's answer is unknown, the order moves to payment_review and keeps its reservation instead of creating a second provider order. The gateway README documents the state machine.

The transition to paid writes an order.paid event in the same ledger update: a transactional outbox. A background publisher sends it to the roboshop.orders exchange using publisher confirms and quorum queues.

Shipping inserts a shipment request before acknowledging, and unique event IDs make redelivery harmless. Five processing failures route a message to a dead-letter queue. Test-mode orders go to an audit queue and never create shipments. kafka-bridge copies both kinds to Kafka topics for analytics.

Checkout never waits for shipping or Kafka. If RabbitMQ is down, events wait in the outbox.

Containers, Kubernetes and GitOps

Each service builds from its own directory with its own Dockerfile. Several Dockerfiles run the service's tests during the build. Per-service GitHub Actions workflows call a shared publish-ecr.yml, which builds linux/arm64 images on a self-hosted runner and pushes :latest to Amazon ECR.

Manifests sit beside each service, and the Argo CD catalog defines one Application per workload. None auto-syncs, and pruning and deletion are disabled (sync options). Application Deployments use read-only root filesystems plus liveness and readiness probes. /ready on web checks only core gateway dependencies, never AI providers.

These manifests describe intent, not live state. The tools repository notes that its Argo CD script creates missing Applications but never edits existing ones, so a live cluster can drift from Git.

Telemetry paths. RoboShop services send OTLP spans to the OpenTelemetry Collector and then Jaeger; Prometheus scrapes /metrics; Alloy reads pod logs into Loki; Grafana queries Prometheus and Loki. Moks can also send metadata-only model spans to Langfuse Cloud when credentials exist.

Figure 4. Telemetry, deployed from the tools repository. Arrows follow the data.

Benefits, and the conditions behind them

Independent deployment and rollback. Each service has its own image, workflow and Argo CD Application. The web consolidation was planned as a side-by-side rollout with an ingress switch and a documented rollback. This depends on backward-compatible contracts; for example, Moks accepts both old and new history formats during an upgrade. Rollback is weaker than it looks, because every image is a mutable :latest.

Smaller change scopes and clear ownership. Each folder owns its dependencies, Dockerfile and tests, and the repository documents a future split into separate repositories. One person maintains everything today, so team ownership is something this design enables rather than demonstrates.

Languages suited to the task. Go's standard library is enough for a small rate cache. Java fits the inherited shipping design. Python carries the AI libraries (LangGraph, FastMCP, the AWS SDK). The condition is paying for each runtime: base images, dependency updates and expertise.

Independent scaling. Shipping supports multiple replicas, and currency-service keeps a per-replica cache. The gateway cannot scale: its JSON ledger allows one writer, so it runs one replica with Recreate. A PostgreSQL ledger backend exists but is not the default. Every Deployment runs one replica and no autoscaler is defined. Scaling requires externalized state first.

Fault isolation and graceful degradation. Stopping Moks hides the widget without affecting shopping. A currency outage leaves original prices. Kafka problems never block checkout. This works because of explicit limits: a shared 23-second downstream budget, at most eight concurrent blocking handlers, and readiness that excludes optional features. Kubernetes restarts pods, but it does not make an application resilient: the gateway is still a single point of failure, and sessions fail closed when user-service is down.

Testing through explicit contracts. The gateway has user-service contract tests. MCP tests drive a real client against a synthetic gateway, and NGINX tests run against a mock gateway. These are hand-written fixtures, not consumer-driven contracts, so they protect only what someone remembered to assert.

Configuration, credentials and permissions. The Razorpay secret lives only in payment-service, SMTP credentials only in user-service, and Moks has no database access. Internal calls use per-purpose service tokens. In the lab, though, most application secrets share one Kubernetes Secret and MongoDB shares one credential. Isolation comes from which keys each Deployment mounts, not separate secret stores.

Observability across services. NGINX, the gateway and MCP propagate W3C Trace Context, so one chat turn can appear as one trace (context propagation). The condition is instrumenting every hop. Jaeger storage here is in-memory, and no Alertmanager is installed, so alerts are evaluated but never delivered.

Reusing business APIs through agents and MCP. MCP tools call the same gateway endpoints as the browser, so authorization stays in one place. That holds only while the gateway API stays stable and role-checked. Exposing MCP over the internet would also need OAuth 2.1, which is not implemented.

Incremental modernization. Product ingestion and recommendations were folded into catalogue-service, Finder and the shopping agent into Moks, and frontend and NGINX into web. Modernization sometimes meant fewer services. Each step required deliberate operator clean-up, because Argo CD does not prune old workloads.

Trade-offs to weigh honestly

  • Latency and partial failure. A chat turn crosses the gateway, Moks, catalogue-service, a configuration check and a model. Each hop needs a timeout and a distinct failure outcome: feature-unavailable, assistant-busy or LLM_NOT_CONFIGURED.
  • Debugging. Without trace propagation you are reading many separate logs. Access logs deliberately omit bodies, so correlation IDs carry the investigation.
  • Compatibility and coordinated migrations. Some changes need an order: upgrade Moks before the gateway; start web before switching the ingress.
  • Eventual consistency and duplicates. Delivery is at least once. Shipping deduplicates by event ID, and Kafka consumers must do the same.
  • Data ownership and cross-service transactions. Checkout spans the ledger, Redis, Razorpay and user-service with no distributed transaction. Reservations, idempotency keys and reconciliation replace it, and abandoned checkouts keep their reservations because nothing releases them automatically.
  • Operational overhead. Eleven application images, eleven CI workflows, an Argo CD catalog, ECR token refreshes and secrets to manage.
  • Resource cost on a small lab. Kafka alone requests 768 MiB, before RabbitMQ, PostgreSQL, MongoDB and the observability stack.
  • Distributed-monolith risk. Every browser API call passes through the gateway, and catalogue reads a collection the gateway writes. If most changes touch several services at once, the system has a monolith's coupling plus network costs.

When a modular monolith would be simpler

For one team on one release cadence, a modular monolith is often the better start (MonolithFirst). Catalogue, accounts, cart and checkout as modules in one process with one database get in-process calls and ACID transactions for checkout. That removes much of the reservation and reconciliation machinery described above.

RoboShop AI's gateway already behaves like a small modular monolith for checkout. Splitting further is worthwhile here because practising distributed operations is the point of the project. For a small shop, I would start with a monolith and extract a service only when a boundary proves itself.

Practical takeaways

  • Draw boundaries around data and rates of change, then accept that shared infrastructure blurs them.
  • Keep authority in one place. Here the gateway owns stock and payment state, and everything else defers to it.
  • Make optional features optional in readiness, routing and UI, not just in documentation.
  • Design for at-least-once delivery from the first event.

Proposed next steps, not yet implemented:

  • pin images by digest;
  • move the gateway to the PostgreSQL ledger so it can run replicas;
  • give each service its own database credential;
  • add durable trace storage and alert delivery.

References

Project sources (pinned to the reviewed commit)

General references