Microservice Architecture
The Decision That Took 18 Months to Get Right
I have mentioned already in Monolithic Architecture that my POS system started as a monolith and that the first attempt at microservices failed badly. Here is the full story of how I eventually got it right — and what I understand now that I did not then.
The first split happened when I noticed a pattern: the chatbot service was consuming disproportionate CPU during peak hours, and its long-running LLM inference calls were blocking resources needed by real-time order processing. The chatbot's workload was genuinely different from the order processing workload. That was a real domain boundary, with a real operational reason to separate.
That distinction — operational pressure + domain boundary — is the signal I now look for before splitting a service.
Table of Contents
What Is a Microservice?
A microservice is a small, independently deployable service focused on a single business capability. Independently deployable means I can release a new version of the Inventory service without touching the Order service.
Key characteristics:
Single responsibility — owns one bounded context
Independent deployment — its own CI/CD pipeline, its own release cycle
Data ownership — its own database, not shared with other services
Communication through APIs or events — no shared libraries for business logic, no shared databases
The Decomposition Decision
How do I decide what becomes a service? I use three criteria:
1. Domain Boundary
If two parts of the system are owned by conceptually separate bounded contexts — orders vs. inventory vs. payments — they are candidates for separation. Domain boundaries are the natural seams.
2. Independent Scaling Need
If one part needs to scale differently from the rest, that is a signal. The chatbot service needed GPU-adjacent resources and handled long-running requests. The order service needed low latency and high throughput. These are different resource profiles.
3. Independent Deployment Rhythm
If different parts of the system change at very different rates — the chatbot model and prompt logic changed weekly; the payment integration changed monthly; the order schema almost never changed — that is a reason to separate.
All three do not need to align for every split. But I am suspicious of splits that satisfy none of them.
My POS Microservices Architecture
Here is what each service in the POS system owns:
Auth Service
Authentication, JWT, tenant isolation
PostgreSQL
Python (FastAPI)
POS Core
Order lifecycle, order items, receipts
PostgreSQL
Python (FastAPI)
Inventory
Products, stock levels, stock reservations
PostgreSQL
Python (FastAPI)
Payment
Payment processing, transaction records
PostgreSQL
Python (FastAPI)
Restaurant
Tenant management, table configuration, staff
PostgreSQL
Python (FastAPI)
Chatbot
LLM conversation history, context, responses
MongoDB
Python (FastAPI)
The chatbot uses MongoDB because its data model is document-oriented (conversation threads with variable structure), while all other services have relational data with clear schemas.
Inter-Service Communication
Synchronous (REST)
For real-time calls where the caller needs an immediate response:
Asynchronous (Events)
For operations where the caller does not need to wait for completion:
The Notification service and the Restaurant service subscribe to pos.events and handle order.placed independently — without any coupling to POS Core.
Data Ownership
The hardest part of microservices is accepting that each service owns its data and no other service touches it directly.
This means:
No shared database tables between services
No cross-service JOINs in SQL
If Service A needs data from Service B, it calls Service B's API
In practice, this led me to duplicate certain read-optimised data. For example, the Chatbot service caches product names so it can answer questions about the menu without calling the Inventory service on every user message. When the Inventory service updates a product name, it publishes a product.updated event, and the Chatbot service updates its local cache.
This is eventual consistency — the chatbot's view of product data may be seconds behind, but that is acceptable for its use case.
Service Discovery and Routing
In development, I use Docker Compose with service names as DNS hostnames:
In production on Kubernetes, service discovery happens through cluster DNS — services refer to each other by service name, and Kubernetes resolves it to the correct pod IP.
Deployment with Docker and Docker Compose
Each service has its own Dockerfile:
The benefit: I can rebuild and redeploy only the Auth service without touching any other container. That independent deployment cycle is one of the practical payoffs of microservices.
Operational Realities
Microservices introduce complexity that a monolith does not have:
Distributed tracing
Correlation IDs passed in request headers across all services
Partial failures
Circuit breakers + retry with exponential backoff on HTTP clients
Cross-service debugging
Structured logging with tenant_id and correlation_id in every log line
Schema evolution
Versioned APIs (/api/v1/, /api/v2/), never break existing contracts
Local development
Docker Compose brings up all 6 services + databases in one command
None of these are solved once. They require ongoing attention.
When Not to Use Microservices
I would not start with microservices if:
The team is fewer than 3–4 people
The domain boundaries are not understood yet
There is no CI/CD pipeline (deploying 6 services manually is worse than a monolith)
The system is a proof of concept or early-stage product
Start with a well-structured monolith, identify the natural seams, then extract services when you feel the operational pressure described in the decomposition section.
Lessons Learned
The first split should happen when you feel operational pain, not when you read about microservices.
Own your data completely or do not split the service. Shared database microservices are distributed monoliths.
HTTP between services is a network call that can fail. Every inter-service call needs a timeout, a retry policy, and a fallback.
Observability is not optional. Without distributed tracing and structured logs, debugging a failure that involves three services is nearly impossible.
Microservices scale development teams, not just load. The real benefit of microservices is that two teams can work independently. If you do not have two teams, you may not need the separation.
Last updated