Microservice Architecture
π Microservice Architecture β Build Scalable Apps Step-by-Step (with examples, pitfalls & checklist)
Microservices turn a huge, monolithic app into a team-friendly, independently deployable system of small services. This blog explains the terminology, core concepts, a practical step-by-step guide (with a working example using containers), common mistakes to avoid, and a handy checklist so you can ship confidently. Letβs go! π₯
β Why microservices?
- Scale parts of your system independently (scale the payment service, not the whole app). π
- Teams can own services, choose appropriate tech stacks, and deploy independently. π₯
- Fault isolation β a failure in one service is less likely to take everything down. π‘οΈ
But microservices add complexity β networking, deployment, observability and distributed data. Use them when benefits outweigh the operational cost.
π§° Key Terminology (quick cheat sheet)
- Service β an independently deployable app that performs one business capability (e.g.,
user-service,order-service). - API / HTTP contract β how services talk (REST, gRPC, GraphQL). π
- API Gateway β single entry point that routes, authenticates, rate-limits requests.
- Service Discovery β how services find each other (DNS, Consul, Kubernetes Service). π
- Load Balancer β distributes traffic across instances. βοΈ
- Circuit Breaker β prevents cascading failures (Hystrix, resilience patterns). π
- Saga β pattern to manage distributed transactions (compensating actions). π
- Event Bus / Message Broker β async comms (RabbitMQ, Kafka). π
- Observability β logs, metrics, traces for debugging (Prometheus, Grafana, Jaeger). π
- CI/CD β continuous integration and deployments for services (GitHub Actions, Jenkins, GitLab CI). π
- Container & Orchestration β Docker, and Kubernetes for running services at scale. π³β‘οΈβΈοΈ
π§© Core Concepts & Design Principles
- Bounded Context β each service owns data & logic for a specific business area.
- Single Responsibility β keep services small and focused.
- Decentralized Data β each service has its own datastore to avoid schema coupling. ποΈ
- API Contracts & Versioning β maintain backwards compatibility; version your APIs. π
- Prefer async for long-running tasks β use events/queues to decouple. β³
- Idempotency β make endpoints safe to retry. β»οΈ
- Observability by default β instrument services for logs/metrics/traces. π
- Infrastructure as Code β automate deployments, not manual steps. π§±
π οΈ Step-by-Step Guide β Hands-on Example (Rails API services + Docker Compose)
Weβll build a minimal example with:
user-service(Rails API)product-service(Rails API)api-gateway(NGINX or a tiny Express / Rails gateway)postgresDBs for servicesdocker-composeto run locally
Note: you can substitute Rails with Node/Python/Go depending on team preference.
1) Design & define boundaries
Example responsibilities:
user-service: user signup, profile, auth (or just user data if auth is centralised)product-service: product catalog, search- Each service owns its DB β
users_db,products_db.
2) Create two simple Rails API apps (local development)
Commands (run for each service folder):
# create user-service (Rails API only)
rails new user-service --api -d postgresql
cd user-service
rails g scaffold User name:string email:string
# configure database.yml to use ENV vars for DB_HOST/DB_USER/DB_PASS
Same for product-service:
rails new product-service --api -d postgresql
cd product-service
rails g scaffold Product name:string price:decimal
Important: In production, youβd likely separate auth into its own service or use a third-party identity provider (Auth0, Keycloak).
3) Dockerize each service
Example Dockerfile for a Rails API (user-service):
FROM ruby:3.2-slim
# system deps
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client build-essential
WORKDIR /app
COPY Gemfile* ./
RUN bundle install --jobs 4
COPY . .
ENV RAILS_ENV=production
# Precompile assets if any (API apps generally don't)
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
Make sure database.yml uses ENV variables (ENV['DB_HOST'], ENV['DB_USER'], ENV['DB_PASS'], ENV['DB_NAME']).
4) docker-compose for local testing
docker-compose.yml (simplified):
version: "3.8"
services:
user-db:
image: postgres:15
environment:
POSTGRES_DB: users_db
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- user-db-data:/var/lib/postgresql/data
product-db:
image: postgres:15
environment:
POSTGRES_DB: products_db
POSTGRES_USER: product
POSTGRES_PASSWORD: password
volumes:
- product-db-data:/var/lib/postgresql/data
user-service:
build: ./user-service
depends_on:
- user-db
environment:
DB_HOST: user-db
DB_NAME: users_db
DB_USER: user
DB_PASS: password
RAILS_ENV: development
ports:
- "3001:3000"
product-service:
build: ./product-service
depends_on:
- product-db
environment:
DB_HOST: product-db
DB_NAME: products_db
DB_USER: product
DB_PASS: password
RAILS_ENV: development
ports:
- "3002:3000"
api-gateway:
image: nginx:stable
volumes:
- ./gateway/nginx.conf:/etc/nginx/nginx.conf:ro
ports:
- "8080:80"
depends_on:
- user-service
- product-service
volumes:
user-db-data:
product-db-data:
Example gateway/nginx.conf routes /users to user-service and /products to product-service by proxy_pass.
5) Service communication patterns
- Synchronous (HTTP) β gateway -> user-service -> product-service if needed. Use REST/gRPC. Simple but couples latency/failures.
- Asynchronous (events) β product-service emits
product.createdevent to a message broker; interested services consume. Good for decoupling.
For local dev, start with HTTP and add a message broker (RabbitMQ/Kafka) as you evolve.
6) Database & migrations with Docker Compose
Run migrations in each service container:
docker-compose build
docker-compose up -d
# run migrations inside the rails container (example)
docker-compose exec user-service rails db:create db:migrate
docker-compose exec product-service rails db:create db:migrate
(You can script this into your compose / entrypoint for automation.)
7) Health checks & readiness probes
Expose a /health endpoint that returns 200 when the service is healthy (DB connected, essential subsystems OK). Later map these to Kubernetes readiness/liveness probes.
8) Logging, metrics & tracing (observability)
- Expose structured logs (JSON) β centralized logging (ELK/EFK, Loki). π
- Export metrics to Prometheus (app exposes
/metrics). π - Add distributed tracing (OpenTelemetry β Jaeger) to follow requests across services. π
Instrument early β debugging distributed systems without traces is painful.
9) CI/CD pipeline
- Build & test service image per PR.
- Run contract tests and integration tests.
- Push images to registry (Docker Hub, ECR).
- Deploy via Helm/Kubernetes manifests or managed platform. π
10) Deploy to Kubernetes (optional basic manifest example)
A very simple Deployment + Service for user-service:
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 2
selector:
matchLabels: { app: user-service }
template:
metadata:
labels: { app: user-service }
spec:
containers:
- name: user-service
image: yourrepo/user-service:latest
envFrom:
- secretRef: { name: user-service-secrets }
ports:
- containerPort: 3000
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
selector: { app: user-service }
ports:
- port: 80
targetPort: 3000
Use HorizontalPodAutoscaler for scaling based on CPU/memory or custom metrics.
β οΈ Common Mistakes to Avoid
- Starting with microservices too early β pick monolith β modularize β split when needed. βοΈ
- Sharing a central database across services β causes tight coupling. β
- No contracts or API versioning β breaks clients unexpectedly. π¨
- Poor observability β no logs/traces/metrics = debugging nightmare. π₯
- Insecure service communication β no mTLS, no auth between services. Lock it down. π
- Tight coupling between services β avoid sync chains that create single points of latency/failure. π§©
- Ignoring idempotency & retries β leads to duplicate side effects. π
- Ad-hoc deployments β manual deploys cause drift. Use IaC and CI/CD. βοΈ
β Microservices Checklist (copyable)
- Bounded contexts defined & responsibilities clear
- Each service owns its own datastore (no cross-service DB writes)
- API contract documented (OpenAPI / Swagger) and versioning plan exists
- Health & readiness endpoints implemented
- Centralized logging in place (structured logs)
- Metrics exposed (Prometheus-compatible)
- Distributed tracing enabled (OpenTelemetry/Jaeger)
- CI/CD pipeline builds and tests images automatically
- Automated DB migrations strategy (zero downtime)
- Circuit breaker / timeout rules applied for remote calls
- Backoff, retry, and idempotency implemented for retryable operations
- Authentication & authorization between services (service accounts, mTLS)
- Secrets management in place (Vault, K8s Secrets)
- Disaster recovery plan and backups for critical data
- Resource limits and autoscaling policies defined
- Load balancer and ingress configured for traffic routing
- Security scanning for images and dependencies enabled
π§ͺ Example: Minimal /users endpoint (Rails controller snippet)
app/controllers/users_controller.rb:
class UsersController < ApplicationController
def index
users = User.all
render json: users
end
def create
user = User.new(user_params)
if user.save
# optionally publish event to message broker asynchronously
render json: user, status: :created
else
render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:name, :email)
end
end
Make this API idempotent for operations that can be retried (e.g., by checking request idempotency keys).
π Tools & Tech Suggestions
- Containers: Docker
- Local orchestration: Docker Compose
- Production orchestration: Kubernetes (EKS/GKE/AKS)
- Message brokers: RabbitMQ, Kafka (for high throughput)
- Observability: Prometheus, Grafana, Jaeger, ELK/Loki
- API contracts: OpenAPI/Swagger
- CI/CD: GitHub Actions, GitLab CI, Jenkins
- Secrets: HashiCorp Vault, cloud provider secrets manager
Final tips (short & spicy) πΆοΈ
- Start small: split a single module first (e.g., move the catalog out of the monolith).
- Automate everything: builds, tests, deployments, rollbacks.
- Make debugging easy: structured logs + traces + correlation IDs.
- Invest in good developer DX β if developing and running services is painful, teams will avoid best practices.
© Lakhveer Singh Rajput - Blogs. All Rights Reserved.