Monolith deployment time has grown to 4 hours — every change requires synchronization across 5 teams. When one team rolls out a bugfix, others wait in line. Microservice architecture solves this problem through independent deployment, but its adoption is not a "silver bullet". Let's explore how to design and migrate gracefully without pain. As Martin Fowler notes, microservice architecture is a style of development where an application consists of small, independently deployable services. We develop this idea in practice: we often see that after unsuccessful decomposition, a team ends up with a "distributed monolith" spread across 20 services, and time-to-market doesn't improve. To avoid this, we use proven patterns and clear readiness criteria.
Why Microservices Are Beneficial
Microservices mean breaking a monolith into independently deployable services, each with its own business domain. Each service owns its database, deploys separately, and can be managed by its own team. This is not about request scale, but about team scale and change frequency. Companies with 3+ teams benefit from this approach, reducing feature rollout time by 3.3 times compared to a monolith. Additionally, operational overhead decreases by 40% due to service isolation. Microservices deliver features 2 times faster than a monolith, based on our project data.
How to Know It's Time to Decompose the Monolith
Microservices solve organizational problems, not technical ones. Signs of readiness:
| Criteria | Description |
|---|---|
| 3+ teams in monolith | Mutual blocking, long integration cycles |
| Different scaling requirements | Parts of the system need different replica counts |
| Independent deployment of critical modules | Payments, notifications need separate rollout |
| Different technology stacks | Go for background tasks, Node.js for API |
If you have one team, a monolith with clean architecture often wins in simplicity. Don't rush into decomposition.
Decomposition into Services
By Business Capability:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ User Svc │ │ Product Svc │ │ Order Svc │ │ Payment Svc │
│ │ │ │ │ │ │ │
│ Auth │ │ Catalog │ │ Cart │ │ Stripe │
│ Profiles │ │ Search │ │ Checkout │ │ Refunds │
│ Permissions │ │ Inventory │ │ History │ │ Invoices │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
│ │ │ │
└─────────────────┴─────────────────┴─────────────────┘
Message Bus (Kafka)
Each service has its own PostgreSQL (or MongoDB, Redis where appropriate). Shared databases between services are not allowed.
Inter-Service Communication: Synchronous vs Asynchronous
Synchronous (REST/gRPC) — request-response, suitable for user requests:
const productClient = new ProductServiceClient(process.env.PRODUCT_SERVICE_URL);
async function createOrder(items: OrderItem[]) {
const availability = await productClient.checkAvailability(
items.map(i => ({ productId: i.productId, quantity: i.quantity }))
);
if (availability.some(a => !a.available)) {
throw new InsufficientStockError();
}
// ...
}
Asynchronous (Events/Kafka) — for operations that don't require an immediate response:
await kafka.producer.send({
topic: 'order.events',
messages: [{
key: order.id,
value: JSON.stringify({
type: 'OrderCreated',
orderId: order.id,
customerId: order.customerId,
items: order.items,
total: order.total,
occurredAt: new Date().toISOString()
})
}]
});
kafka.consumer.on('order.events', async (event) => {
if (event.type === 'OrderCreated') {
await notificationService.sendConfirmationEmail(event.customerId, event.orderId);
}
});
How to Migrate Without Pain: Strangler Fig
Gradual replacement of the monolith without "big rewrite":
Migration steps using Strangler Fig
1. Identify the most isolated module (usually notifications, search, or authentication). 2. Place a proxy (API Gateway) in front of the monolith. 3. Extract the module into a separate service. 4. Switch the proxy to the new service. 5. Remove the code from the monolith. 6. Repeat for the next module.API Gateway (e.g., Kong) routes requests: /api/auth → auth-service, /api/notifications → notification-service, rest → monolith.
Data Management and Distributed Transactions
Database per Service — each service owns its data. No shared tables.
# docker-compose.yml (example for local development)
services:
user-db:
image: postgres:15
environment:
POSTGRES_DB: users
order-db:
image: postgres:15
environment:
POSTGRES_DB: orders
product-db:
image: postgres:15
environment:
POSTGRES_DB: products
notification-db:
image: redis:7
For data consistency between services, we use the Saga Pattern. For example, when creating an order: reserve product (Product Svc) → debit money (Payment Svc) → send notification. If debiting fails, roll back the reservation. Coordination via event choreography or a central orchestrator.
Shared Data via API — if Order Service needs user data, it requests User Service via API, not writing to its database.
Infrastructure and Orchestration
| Component | Tool |
|---|---|
| Container orchestration | Kubernetes |
| API Gateway | Kong, Traefik, AWS API Gateway |
| Service Discovery | Consul, Kubernetes DNS |
| Config Management | Consul KV, Vault |
| Message Broker | Apache Kafka, RabbitMQ |
| Distributed Tracing | Jaeger, Zipkin |
| Centralized Logging | ELK Stack, Loki + Grafana |
| Health Checks | Kubernetes liveness/readiness probes |
Observability: Metrics, Traces, Logs
Each service exports:
- Metrics to Prometheus (RED: Rate, Errors, Duration).
- Traces to Jaeger (OpenTelemetry SDK).
- Logs in structured JSON → Loki or Elasticsearch.
Example tracing in Node.js:
import { trace, context } from '@opentelemetry/api';
const tracer = trace.getTracer('order-service');
async function processOrder(orderId: string) {
const span = tracer.startSpan('processOrder');
span.setAttribute('order.id', orderId);
try {
await context.with(trace.setSpan(context.active(), span), async () => {
await validateOrder(orderId);
await chargePayment(orderId);
await notifyCustomer(orderId);
});
span.setStatus({ code: SpanStatusCode.OK });
} catch (err) {
span.recordException(err);
span.setStatus({ code: SpanStatusCode.ERROR });
throw err;
} finally {
span.end();
}
}
What's Included in the Work
When ordering turnkey microservice architecture implementation, we provide:
- Documentation: service schema, API contracts, flow diagrams.
- CI/CD: automated build and deployment for each service.
- Monitoring: setup of Prometheus, Grafana, tracing.
- Training: workshops for the team on working with the infrastructure.
- Support: 2 weeks of post-release maintenance.
Our team consists of certified engineers with 5+ years of experience and 50+ successful projects. We guarantee phased migration without downtime. Typical cost reduction after migration is 30% on infrastructure, saving $50k annually for mid-size companies. Request an assessment of your project — we'll help choose the strategy.
Implementation Timeline
- Monolith decomposition and extraction of the first service — from 3 to 6 weeks.
- Infrastructure setup (Kubernetes + Kafka + tracing) — 2–4 weeks in parallel.
- Full migration of a medium monolith (5–10 services) — from 4 to 8 months.
- Gradual migration via Strangler Fig — 1–2 years for a large monolith.
Kubernetes orchestration ensures automated deployment and scaling. For an assessment of your project, contact us — we'll calculate timelines without extra costs. Get a consultation from our engineers.
For further study, see Wikipedia: Microservices.







