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
| Service | Responsibility | Dependencies / data ownership | Deployment unit |
|---|---|---|---|
web | Static TypeScript storefront plus NGINX reverse proxy | Proxies /api/*, /moks/* and /api/currency/rates; no data | Deployment web |
ai-gateway (Python, FastAPI) | Public API, session checks, admin roles, checkout state machine, event outbox | MongoDB settings, chat history and product edits; JSON order ledger on a volume; calls all domain services | Deployment, 1 replica, Recreate |
user-service (Python) | Accounts, sessions, MFA, password recovery, purchase history | MongoDB user collections; Amazon SES for email | Deployment |
catalogue-service (Node.js) | Product reads, search, keyword recommendations, explicit imports | Reads MongoDB products | Deployment |
cart-service (Python) | Cart lines with compare-and-set revisions | Redis | Deployment |
payment-service (Node.js) | Razorpay order creation, signature and capture checks | Razorpay API; no database | Deployment |
shipping-service (Java) | Locations, delivery quotes, shipment requests | Dedicated PostgreSQL; consumes RabbitMQ | Deployment, plus a PostgreSQL Deployment |
currency-service (Go) | Display exchange rates from the ECB feed | In-memory cache only; optional | Deployment |
moks (Python, LangGraph) | Shopping-assistant widget and agent engine | catalogue-service; Bedrock or Ollama; no database credentials | Deployment, optional |
mcp-server (Python, FastMCP) | MCP tools for search, compare and cart | Calls the gateway with the caller's session; no data | Deployment, ClusterIP and NetworkPolicy |
kafka-bridge (Python) | Copies order events from RabbitMQ to Kafka | RabbitMQ queue, Kafka topics | Container 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
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 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.
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-busyorLLM_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
webbefore 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)
- roboshop-ai repository and its service READMEs
- web, ai-gateway, moks, mcp-server, shipping-service, kafka-bridge
- Observability stack and shared service configuration in the tools repository
General references
- Martin Fowler and James Lewis, Microservices; Martin Fowler, MonolithFirst
- Chris Richardson, Transactional outbox pattern
- RabbitMQ, Publisher confirms and Quorum queues
- Kubernetes, Deployments and Probes
- Argo CD, Sync options
- W3C, Trace Context; OpenTelemetry, Context propagation
- Model Context Protocol specification
- Instana, Robot Shop



Discussion comments
No comments yet. Start the discussion.