Microservices architecture splits one large application into a set of small, independently deployable services, each owning a single business capability and its own data. Done well, it lets teams ship features without waiting on each other, scale only the parts of the system that are under load and contain failures before they take everything down. Done badly, it produces a “distributed monolith”: all the complexity of a network of services with none of the independence.
Contents
At CodeWiz we design and build microservices systems in Node.js, Laravel/PHP, Go and Python – and, just as often, we help companies decide whether they need microservices at all. We draw service boundaries around your business domains, set up the API gateway, choose how services talk to each other, and design for scalability, observability and failure from the first day rather than after the first outage.
This service is for product companies whose monolith has become a bottleneck for releases, SaaS platforms with uneven load across features, and teams that are growing beyond the point where everyone can safely change the same codebase. If deployments require coordinating half the company, or one slow report brings the whole application down, a well-planned move to services is usually worth evaluating.

Service Decomposition: Drawing the Right Boundaries
Service decomposition is the most important decision in any microservices architecture, and the hardest to undo. Boundaries drawn around technical layers (“the database service”, “the validation service”) create chatty services that must always change together. Boundaries drawn around business capabilities – catalog, ordering, billing, notifications – create services that can evolve on their own.
How we find the boundaries
- Event storming workshops with your domain experts: we map business events (“order placed”, “payment captured”, “parcel shipped”) on a timeline and look for natural clusters.
- Bounded contexts from domain-driven design: the word “customer” means something different to sales, billing and support, and that difference often marks a service boundary.
- Change-coupling analysis of your Git history: files that always change in the same commits belong together, no matter which folder they live in.
- Load and team structure: parts with very different scaling needs or owned by different teams are strong candidates for separation (Conway’s law works in both directions).
Rules we apply to every service
- Each service owns its data. Other services never read its tables directly – they use its API or its events.
- A service should be deployable without deploying anything else. If two services always ship together, they are one service.
- Start coarse. Splitting a service later is easy; merging ten nano-services that share one database is not.
Deliverables: a context map, services with their responsibilities and data, contracts and a migration order.
Monolith, Modular Monolith or Microservices?
Microservices are a trade: you gain independent deployment and scaling, and you pay with network calls, eventual consistency and much heavier operations. For many products a modular monolith – one deployable application with strictly separated internal modules – delivers most of the benefits at a fraction of the cost, and it can be split into services later along the same module lines.
| Criterion | Classic monolith | Modular monolith | Microservices |
|---|---|---|---|
| Deployment | One unit, all or nothing | One unit, clean module boundaries | Each service independently |
| Scaling | Whole application | Whole application | Per service, based on its own load |
| Data consistency | ACID transactions everywhere | ACID transactions, schema per module | Eventual consistency, sagas between services |
| Operational overhead | Low | Low | High: orchestration, tracing, many pipelines |
| Failure isolation | Weak | Weak to medium | Strong, if designed for it |
| Best fit | Small teams, early products | Growing products, one or two teams | Several teams, uneven load, frequent independent releases |
Choosing microservices should be a conclusion, not a starting point.
API Gateway Setup for Microservices
Clients should not need to know how many services sit behind your API. An API gateway gives web, mobile and partner clients a single entry point and takes care of the cross-cutting concerns every service would otherwise reimplement: routing, TLS termination, authentication, rate limiting, request size limits, CORS and access logging.
What we configure
- Routing and versioning by path or host, so services can be moved, split or replaced without breaking clients.
- Authentication at the edge: the gateway validates JWTs or API keys once and forwards verified identity to services, which still enforce their own authorisation rules.
- Rate limits and quotas per client or plan – the techniques are covered in our guide to API rate limiting.
- Backend for frontend (BFF) layers when a mobile app and a web dashboard need very different payloads, so neither client has to call five services per screen.
Depending on your stack we use Kong, Traefik, Envoy-based gateways (including the Kubernetes Gateway API), NGINX, or a managed option such as AWS API Gateway. A declarative configuration kept in Git keeps the gateway reviewable and reproducible:
_format_version: "3.0"
services:
- name: orders
url: http://orders.internal:8080
routes:
- name: orders-api
paths: [/api/v1/orders]
plugins:
- name: jwt
- name: rate-limiting
config: { minute: 600, policy: local }
- name: catalog
url: http://catalog.internal:8080
routes:
- name: catalog-api
paths: [/api/v1/products]One warning we repeat often: the gateway must stay thin. Business logic placed in gateway plugins becomes a new monolith that every team has to coordinate around.

Service Communication: Synchronous and Asynchronous
How services talk to each other determines how the system behaves under load and during failures. Every network call can be slow or fail, so communication has to be designed, not improvised.
| Style | Typical tools | Strengths | Trade-offs |
|---|---|---|---|
| REST over HTTP | JSON, OpenAPI | Universal, easy to debug, cacheable | Verbose payloads, temporal coupling |
| gRPC | Protocol Buffers, HTTP/2 | Fast, strongly typed contracts, streaming | Harder to inspect, needs gateway translation for browsers |
| Asynchronous messages | RabbitMQ, Kafka, Redis Streams, NATS | Decoupled in time, absorbs spikes, natural for events | Eventual consistency, harder end-to-end debugging |
| GraphQL federation | Apollo Federation, GraphQL Mesh | One graph for clients over many services | Extra layer to operate, query cost control needed |
Our default rules
- Use synchronous calls for queries that need an answer now; use events for things that “have happened” and that other services may react to.
- Avoid long synchronous chains. If a request needs four services in a row, availability multiplies down and latency adds up.
- Every call has a timeout, retries with exponential backoff and jitter only for idempotent operations, and a circuit breaker that fails fast when a dependency is down.
- Contracts are explicit and versioned: OpenAPI or Protobuf definitions in the repository, with contract tests (for example Pact) in CI.
A minimal Node.js example of a call protected by a timeout, a circuit breaker and a fallback:
import CircuitBreaker from 'opossum';
const getStock = (sku) =>
fetch(`http://inventory.internal:8080/stock/${sku}`, {
signal: AbortSignal.timeout(800),
}).then((res) => {
if (!res.ok) throw new Error(`inventory ${res.status}`);
return res.json();
});
const breaker = new CircuitBreaker(getStock, {
timeout: 1000,
errorThresholdPercentage: 50,
resetTimeout: 10000,
});
breaker.fallback(() => ({ available: null, stale: true }));
const stock = await breaker.fire('SKU-123');For high-throughput internal traffic we often use gRPC; for anything that crosses team or company boundaries we prefer well-documented REST or events. In larger Kubernetes setups a service mesh such as Linkerd or Istio can add mutual TLS, retries and traffic metrics without code changes – but only when the number of services justifies the extra moving part.
Data Management Across Services
The database is where most microservices projects quietly fail. If services share tables, they are coupled no matter how many repositories you have. We give each service its own schema or database and handle cross-service consistency explicitly:
- Sagas replace distributed transactions: a sequence of local transactions with compensating actions (“release the reserved stock”) when a later step fails.
- Transactional outbox: a service writes its state change and the event describing it in one local transaction, and a relay publishes the event – no lost or phantom messages.
- Read models: when a screen needs data from several services, we build a dedicated, denormalised view fed by events instead of joining across services at request time.
- Idempotent consumers, so a message delivered twice does not charge a customer twice.
When a single service’s data outgrows one database server, sharding or partitioning becomes the next step – we explain the difference in our article on database sharding vs. partitioning.
Scalability Design for Microservices
Scalability is not something you add with more servers at the end; it comes from design decisions made early. Our microservices scalability design focuses on making each service cheap to replicate and safe to run in many copies.
Design principles
- Stateless services: sessions, uploads and caches live in Redis, object storage or the database, so any instance can serve any request.
- Horizontal autoscaling on the signal that actually reflects load – CPU for compute-heavy APIs, queue depth or consumer lag (via KEDA) for workers.
- Caching at the right layer: CDN for public responses, Redis for hot lookups, and HTTP cache headers between services where data allows it.
- Back-pressure and load shedding: bounded queues, concurrency limits and graceful 429/503 responses instead of collapsing under a spike.
- Bulkheads: separate connection pools and worker pools per dependency, so one slow downstream system cannot exhaust every thread.
On Kubernetes, a Horizontal Pod Autoscaler keeps a service between a safe minimum and a cost ceiling:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: orders
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: orders
minReplicas: 2
maxReplicas: 12
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 65We validate the design with k6 or Locust load tests that model real traffic patterns and document where the next bottleneck will appear. Infrastructure choices follow from the numbers; we have written about deployment strategies across DigitalOcean, AWS, Hetzner and bare metal.

Observability: Seeing Inside a Distributed System
In a monolith a stack trace tells you what went wrong. In a microservices system one user request can touch ten services, so you need three signals working together:
- Distributed tracing with OpenTelemetry, exported to Jaeger, Grafana Tempo or a commercial backend, with trace IDs propagated through HTTP headers and message metadata.
- Structured logs in JSON, including the trace ID, collected centrally with Loki, the ELK stack or similar.
- Metrics in Prometheus and Grafana: request rate, error rate and latency per service and endpoint, plus queue lag and saturation.
We define service-level objectives for the critical user journeys and alert on those, not on every CPU spike.
Migrating from a Monolith Step by Step
We almost never recommend a big-bang rewrite. Instead we use the strangler fig pattern: the gateway sits in front of the existing application, and one capability at a time is extracted into a new service while the rest of the traffic still goes to the monolith.
- Put the gateway and observability in front of the monolith, with no functional change.
- Extract the capability with the clearest boundary and highest value – often notifications, search or billing.
- Move its data, keep both sides in sync temporarily, then switch traffic with a feature flag.
- Remove the old code path, measure, and repeat.
The business keeps running, and you can stop whenever the remaining monolith is good enough.
Common Microservices Mistakes We Fix
- A shared database between services, which turns every schema change into a cross-team negotiation.
- Services split by technical layer, so every feature touches five repositories.
- Synchronous call chains without timeouts, where one slow service causes cascading failures.
- Retrying non-idempotent operations and creating duplicate orders or payments.
- No distributed tracing, so incidents are debugged by grepping logs on ten servers.
- Too many tiny services for a small team, with more pipelines than developers.
What Drives the Cost of a Microservices Project
We estimate after an architecture review rather than quoting blindly. The main cost drivers are the number of services and how tangled the current data model is, greenfield versus migrating a live monolith, the platform target (Docker Compose, Swarm or Kubernetes), availability and compliance requirements, and how much CI/CD and observability already exists. More services also mean more to patch and monitor, so we discuss ongoing operations from the start.
How We Work
- Discovery: we review your codebase, data model, traffic, team structure and release pain points, and confirm whether microservices are the right answer.
- Architecture: we run domain workshops, define service boundaries, contracts, communication styles and the data ownership map, and agree on a migration order.
- Foundation: we set up the API gateway, CI/CD per service, container images, tracing, logging and metrics before the first service goes live.
- Build and extract: we implement new services or extract them from the monolith one at a time, behind feature flags.
- Test: contract tests, load tests and failure injection – we deliberately kill instances and slow down dependencies to see how the system reacts.
- Launch and support: gradual traffic switching, close monitoring, runbooks and optional ongoing maintenance.
Why CodeWiz
- Full-stack experience: we build the services ourselves in Node.js, Laravel, Go and Python, as well as the infrastructure they run on.
- Honest architecture advice: tailored solutions instead of one-size-fits-all – if a modular monolith serves you better, we will say so.
- Open-source first: Kubernetes, Traefik, Kong, OpenTelemetry, Prometheus and Grafana keep you free of vendor lock-in.
- Fair pricing: premium development without premium prices, from a team in Banja Luka working remotely with clients locally and internationally.
- Performance and security in the same hands: caching, databases, Cloudflare and hardening are part of our daily work – read more about CodeWiz.
Frequently Asked Questions
When should a company move to microservices?
When several teams are blocked by one codebase, parts of the system have very different scaling needs, or releases are slowed down by coordination. If you have one small team and moderate traffic, a well-structured modular monolith is usually faster and cheaper.
How much does a microservices architecture cost?
It depends on the number of services, whether you are migrating a live system, the target platform and your availability requirements. An architecture review with a migration plan is a small, fixed piece of work; we give a clear estimate for the implementation after it.
How long does a migration from a monolith take?
Extracting the first service, including the gateway and observability foundation, typically takes a few weeks. A full migration is a series of such steps over months, and each step delivers value on its own – you can pause whenever the remaining monolith is good enough.
Do microservices require Kubernetes?
No. A handful of services runs well on Docker Compose or Swarm. Kubernetes becomes worthwhile with many services, autoscaling needs or several teams deploying independently.
Which languages and frameworks do you use for microservices?
Mostly Node.js (Express, Fastify, NestJS), Laravel and PHP, Go and Python. Services can use different languages where it makes sense, but we keep the stack small enough for your team to maintain.
Planning a Microservices Architecture?
Tell us where your current system hurts – slow releases, scaling limits or fragile deployments – and we will propose an architecture that fits your team and budget. Contact us to discuss your project, or explore our microservices architecture services.