Microservices Platform: Deep Dive
This demo deploys five services into Kubernetes: a frontend, a backend API, a worker, a Redis queue, and a PostgreSQL database. Each runs in its own pod. Each can be scaled, updated, and restarted independently. That independence is the entire point of microservices.
This document explains the architecture principles behind that design, why certain trade-offs were made, and how Kubernetes provides the networking and scheduling primitives that make microservices practical.
What Makes This a Microservices Architecture
Section titled “What Makes This a Microservices Architecture”A monolith bundles all functionality into a single deployable unit. A microservices architecture splits that functionality into small, independent services that communicate over the network. This demo has five:
| Service | Role | Image |
|---|---|---|
| Frontend | Serves the HTML dashboard | nginx:1.25-alpine |
| Backend | Returns JSON on API routes | nginx:1.25-alpine |
| Worker | Processes jobs from the queue | busybox:1.36 |
| Redis | Queue and cache layer | redis:7-alpine |
| PostgreSQL | Persistent data store | postgres:16-alpine |
Each service has its own Deployment, its own container image, and its own Service resource. They share nothing except a namespace.
Bounded Contexts and Single Responsibility
Section titled “Bounded Contexts and Single Responsibility”Two principles from Domain-Driven Design shape how you split a monolith.
Bounded contexts define clear boundaries around a business capability. The “order management” context owns everything about orders: the database schema, the API endpoints, the processing logic. In this demo, PostgreSQL holds the orders table, the backend serves the orders API, and the worker processes order-related jobs. They form a bounded context.
Single responsibility means each service does one thing. The frontend serves HTML. It does not query databases. The worker processes jobs. It does not serve HTTP traffic. The backend returns API responses. It does not render HTML pages.
Look at the backend deployment. It has environment variables for both PostgreSQL and Redis, but its nginx config only returns static JSON:
containers:- name: backend image: docker.io/library/nginx:1.25-alpine env: - name: POSTGRES_HOST value: postgres - name: POSTGRES_PORT value: "5432" - name: REDIS_HOST value: redis - name: REDIS_PORT value: "6379"The environment variables tell the backend where to find its dependencies. The backend knows about Redis and PostgreSQL. But the frontend knows about neither. That boundary matters.
Service Discovery via DNS
Section titled “Service Discovery via DNS”In a monolith, one function calls another through a function call. In microservices, one service calls another through the network. Kubernetes makes this work with DNS-based service discovery.
Every Service object gets a DNS entry in the cluster. The Redis Service is defined like this:
apiVersion: v1kind: Servicemetadata: name: redis namespace: microservices-demospec: selector: app: redis ports: - port: 6379 targetPort: 6379Because the Service is named redis in the microservices-demo namespace,
any pod in that namespace can reach it at redis:6379. Pods in other
namespaces can use redis.microservices-demo.svc.cluster.local:6379.
This is how the worker finds Redis:
env:- name: REDIS_HOST value: redis- name: REDIS_PORT value: "6379"The worker does not need the pod’s IP address. It does not need to know
which node Redis runs on. Kubernetes DNS resolves redis to the Service’s
ClusterIP, and kube-proxy routes traffic to the actual Redis pod.
This is fundamentally different from hardcoding IP addresses. If the Redis pod restarts and gets a new IP, the Service continues to work. The DNS name stays the same.
Inter-Service Communication Patterns
Section titled “Inter-Service Communication Patterns”Services can communicate synchronously or asynchronously. This demo uses both patterns.
Synchronous: REST over HTTP
Section titled “Synchronous: REST over HTTP”The Ingress routes HTTP requests to the backend. A client sends a GET
request to /api/orders. The Ingress forwards it to the backend Service.
The backend returns JSON immediately. This is synchronous communication.
spec: ingressClassName: nginx rules: - host: microservices.local http: paths: - path: /api pathType: Prefix backend: service: name: backend port: number: 80Synchronous communication is simple. The caller sends a request, waits for a response, and continues. It works well for read operations and queries.
The downside: if the backend is slow or down, the caller is blocked. Synchronous calls create temporal coupling between services.
Asynchronous: Job Queue via Redis
Section titled “Asynchronous: Job Queue via Redis”The worker polls Redis for jobs. It does not receive HTTP requests. It pulls work from a queue at its own pace. This is asynchronous communication.
command:- /bin/sh- -c- | echo "Worker started, connecting to Redis at $REDIS_HOST:$REDIS_PORT" while true; do echo "[$(date '+%Y-%m-%d %H:%M:%S')] Processing job from queue..." echo "[$(date '+%Y-%m-%d %H:%M:%S')] Job processed successfully" sleep 10 doneIn a production system, the backend would push jobs into Redis (using LPUSH or a Streams command), and the worker would pull them (using BRPOP or XREAD). The queue decouples the producer from the consumer. If the worker is slow, jobs accumulate in Redis. If the worker crashes, jobs persist until it recovers.
Asynchronous communication is essential for work that takes time: sending emails, generating reports, resizing images, running ML inference. You do not want an HTTP request to wait for these operations.
Database-per-Service vs Shared Database
Section titled “Database-per-Service vs Shared Database”This demo uses a single PostgreSQL instance. All services that need data share it. This is the “shared database” pattern.
containers:- name: postgres image: docker.io/library/postgres:16-alpine env: - name: POSTGRES_DB value: ordersdb - name: POSTGRES_USER value: orders - name: POSTGRES_PASSWORD value: orders123The shared database pattern is simpler but creates tight coupling. If the orders schema changes, every service that queries it must update. Migrations become coordination problems.
The alternative is database-per-service: each service owns its own
database instance. The orders service owns ordersdb. The users service
owns usersdb. Services never query each other’s databases directly. They
communicate through APIs.
Trade-offs:
| Approach | Pros | Cons |
|---|---|---|
| Shared DB | Simple, consistent, no duplication | Tight coupling, single point of failure |
| DB per service | Loose coupling, independent schemas | Data duplication, eventual consistency |
For a small demo, the shared database is fine. For a system with 20 teams working on 50 services, database-per-service prevents one team’s schema change from breaking another team’s service.
The API Gateway Pattern
Section titled “The API Gateway Pattern”External clients do not connect to services directly. They connect to the
Ingress, which acts as a simple API gateway. The Ingress routes / to the
frontend and /api to the backend:
paths:- path: /api pathType: Prefix backend: service: name: backend port: number: 80- path: / pathType: Prefix backend: service: name: frontend port: number: 80Without an API gateway, clients would need to know the address of every service. Adding a new service means updating every client. The gateway provides a single entry point. Clients talk to one URL. The gateway routes to the correct service.
In production, the gateway also handles cross-cutting concerns: TLS termination, rate limiting, authentication, request logging. The API Gateway demo covers this in depth with Kong.
The Worker Pattern
Section titled “The Worker Pattern”The worker is a Deployment with no Service. It does not receive inbound traffic. It exists solely to process background work.
Notice that the worker manifest has no Service object. Compare that to the backend, which has both a Deployment and a Service. The worker needs no Service because nothing sends HTTP requests to it. It pulls work from Redis.
This pattern appears everywhere in production systems:
- Email senders that dequeue messages from a queue
- Image processors that read from an object store
- ML inference workers that consume prediction requests
- Report generators triggered by scheduled jobs
Workers scale independently of the API layer. If the queue is backing up, you scale workers:
kubectl scale deployment worker --replicas=5 -n microservices-demoThe backend does not know or care how many workers exist. The queue absorbs the difference.
Health Checking Across Services
Section titled “Health Checking Across Services”The backend exposes a /api/health endpoint:
location /api/health { default_type application/json; return 200 '{"status":"healthy","service":"backend-api","timestamp":"$time_iso8601"}';}Health endpoints serve two purposes. First, Kubernetes uses them for readiness and liveness probes. A liveness probe tells Kubernetes whether to restart the pod. A readiness probe tells Kubernetes whether to send traffic to it. This demo does not configure explicit probes, but production manifests should.
Second, health endpoints enable monitoring. An external system can poll
/api/health and alert when it returns anything other than 200.
In a microservices architecture, health checking is more complex than in a monolith. The backend might be healthy, but if PostgreSQL is down, the backend cannot serve useful responses. A good health check distinguishes between “the process is running” (liveness) and “the service can handle requests” (readiness). Readiness checks should verify downstream dependencies.
12-Factor App Principles in Practice
Section titled “12-Factor App Principles in Practice”The 12-factor app methodology defines principles for building cloud-native applications. This demo demonstrates several.
Factor III: Config. Configuration is injected via environment variables, not hardcoded:
env:- name: POSTGRES_HOST value: postgres- name: POSTGRES_PORT value: "5432"The same container image can run in dev, staging, and production. Only the environment variables change.
Factor IV: Backing Services. PostgreSQL and Redis are “attached
resources” accessed via environment variables. Swapping Redis for a managed
Redis service means changing the REDIS_HOST value. The application code
does not change.
Factor VI: Processes. Each service runs as a stateless process. The frontend and backend store no state in memory between requests. The worker is stateless too. State lives in Redis and PostgreSQL.
Factor VIII: Concurrency. Scaling happens by adding more processes (pods), not by threading within a single process. Need more backend capacity? Add replicas. Need more job throughput? Add workers.
Failure Modes
Section titled “Failure Modes”Microservices introduce failure modes that do not exist in a monolith.
Cascading Failures
Section titled “Cascading Failures”If PostgreSQL goes down, the backend cannot serve order data. If the backend
goes down, the frontend links to /api/orders break. One failure cascades
through dependent services.
In a monolith, a database failure crashes the entire application. In microservices, the blast radius depends on how services handle failures. A well-designed backend returns a cached response or a degraded response when PostgreSQL is unreachable. A poorly designed one returns a 500 error that confuses the frontend.
Circuit Breaking
Section titled “Circuit Breaking”A circuit breaker stops calling a failing service. If the backend notices that PostgreSQL has failed five times in a row, it stops trying for 30 seconds. This prevents wasted resources and gives PostgreSQL time to recover.
This demo does not implement circuit breaking because it uses nginx for the backend (which returns static JSON). In a real system, libraries like Hystrix, resilience4j, or service mesh sidecars (Envoy, Istio) provide circuit breaking.
Partial Availability
Section titled “Partial Availability”A microservices architecture can be partially available. The frontend can serve its HTML page even if the backend is down. The API endpoints on the page will fail, but the page itself loads. In a monolith, the entire application is either up or down.
Designing for partial availability means each service must handle the case where its dependencies are unreachable. Return cached data. Show a “service temporarily unavailable” message. Degrade gracefully.
Scaling Individual Services
Section titled “Scaling Individual Services”Each Deployment can scale independently:
# The backend is overloaded. Scale it.kubectl scale deployment backend --replicas=3 -n microservices-demo
# The queue is growing. Scale the workers.kubectl scale deployment worker --replicas=5 -n microservices-demo
# The frontend is fine. Leave it at 1 replica.This is one of the strongest arguments for microservices. In a monolith, scaling means running more copies of the entire application, including the parts that do not need scaling. In a microservices architecture, you scale only the bottleneck.
The backend Service distributes traffic across all backend pods automatically. The Service acts as a load balancer:
apiVersion: v1kind: Servicemetadata: name: backend namespace: microservices-demospec: selector: app: backend ports: - port: 80 targetPort: 80The selector app: backend matches all pods with that label, regardless
of how many replicas exist. Add a replica, and it immediately starts
receiving traffic.
Monolith vs Microservices Trade-offs
Section titled “Monolith vs Microservices Trade-offs”Microservices are not always better than monoliths. The choice depends on team size, system complexity, and operational maturity.
When a monolith is better:
- Small team (fewer than 10 developers)
- Simple domain with few business capabilities
- Early-stage project where the domain model is still evolving
- Limited operational expertise (no CI/CD, no monitoring, no service mesh)
When microservices are better:
- Multiple teams that need to deploy independently
- Different services have different scaling requirements
- Different services need different technology stacks
- The domain is well-understood and boundaries are clear
The common mistake is starting with microservices. Start with a monolith. Split when the pain of the monolith (deploy conflicts, scaling bottlenecks, team coordination overhead) exceeds the pain of distributed systems (network failures, eventual consistency, operational complexity).
Martin Fowler calls this the “monolith first” approach. It is the safer path because merging microservices back into a monolith is much harder than splitting a monolith into microservices.
Observability in a Microservices World
Section titled “Observability in a Microservices World”A monolith produces one log stream and one set of metrics. A microservices architecture produces N log streams and N sets of metrics. Debugging becomes harder because a single user request might touch five services.
Three pillars of observability address this:
Logs. Each service logs independently. In this demo, you check logs per service:
kubectl logs -f deploy/worker -n microservices-demokubectl logs -f deploy/backend -n microservices-demoIn production, you collect all logs into a central system (EFK stack, Loki, Splunk) so you can search across services.
Metrics. Each service exports metrics: request rate, error rate,
latency, queue depth. Tools like Prometheus scrape these metrics and
Grafana visualizes them. The backend’s /api/stats endpoint hints at
this:
'{"total_orders":5,"pending":2,"processing":2,"completed":1,"queue_depth":3}'Distributed tracing. A trace follows a single request through every service it touches. The Ingress assigns a trace ID. Each service passes it along. Tools like Jaeger or Zipkin collect these traces and show the full request path.
Without all three, debugging a production microservices system is like finding a needle in a haystack while blindfolded.
How the Pieces Fit Together
Section titled “How the Pieces Fit Together”This demo is intentionally simplified. The backend returns static JSON instead of querying PostgreSQL. The worker logs messages instead of actually dequeuing from Redis. This keeps the manifests small and the dependencies minimal.
But the architecture is real. Five services. Three communication patterns (HTTP via Ingress, environment-variable-based service references, queue polling). Two data stores. One namespace providing logical isolation.
The patterns you see here, service discovery via DNS, configuration via environment variables, independent scaling via replica counts, background processing via workers and queues, are the same patterns used in production microservices systems at any scale.
Where to Go Next
Section titled “Where to Go Next”- API Gateway adds Kong in front of services for rate limiting and authentication.
- Event-Driven Kafka replaces the simple Redis queue with Apache Kafka for durable, ordered event streaming.