Everything You Need to Know About Microservices
Microservices are one of the most discussed architecture styles in software, and one of the most misunderstood. Some teams adopt them and get faster, independent releases. Others adopt them and end up with a distributed mess that is harder to change than the monolith they replaced. The difference is rarely the technology. It is whether the team understood what they were trading away.
This article covers what microservices are, why teams choose them, what they cost, how to design and run them, and how to tell whether they fit your situation. It also shows how TDK CLI helps you run a microservice system on your own machine during development.
What is a microservice?
A microservice is a small, independently deployable service that owns one business capability. It has its own code, its own data store (ideally), and a network interface that other services call. A system built this way is a collection of services that cooperate to do the work a single application would do.
Three properties define the style:
- Independent deployment. You can release one service without redeploying the others.
- Single responsibility by business capability. A service is organized around something the business does, such as payments, inventory, or notifications, not around a technical layer such as "the database layer."
- Decentralized data. Each service owns the data it needs. Other services do not reach into its tables.
"Micro" is a misleading word. The goal is not the smallest possible service. It is a service that one team can understand, change, and run without coordinating with everyone else.
Monolith versus microservices
A monolith is a single deployable unit. All modules share one process, one codebase, and usually one database. It is the default for good reasons: function calls are fast, transactions are simple, debugging is straightforward, and there is one thing to deploy.
Microservices trade those advantages for independence:
| Concern | Monolith | Microservices |
|---|---|---|
| Deployment | One artifact, one pipeline | Many artifacts, many pipelines |
| Scaling | Scale the whole app | Scale the hot services only |
| Communication | In-process calls | Network calls (HTTP, gRPC, messages) |
| Data consistency | ACID transactions across modules | Eventual consistency, sagas, or careful design |
| Debugging | Stack traces and a debugger | Distributed tracing and correlated logs |
| Team autonomy | Shared codebase, more coordination | Teams own services end to end |
| Operational overhead | Low | High |
Neither style is better in general. A well-structured monolith usually beats a poorly cut set of microservices. Microservices pay off when the organization, not the code, is the bottleneck.
Why teams choose microservices
- Organizational scale. When dozens of teams work in one codebase, merge conflicts, release trains, and shared-module disputes slow everyone down. Services let each team move at its own pace.
- Independent scaling. A search endpoint under heavy load should not force you to scale the billing code too.
- Technology fit. A service that does image processing may benefit from a different runtime than a service that serves CRUD over a relational database. Microservices make that choice local.
- Fault isolation. A memory leak in the recommendations service should not take down checkout, if the system is designed so that it cannot.
- Faster, safer releases. Smaller changes are easier to test, review, and roll back.
What microservices cost
The benefits above come with real costs. Teams that ignore them are the ones who regret the move.
- Network is unreliable. Every call between services can time out, fail, or succeed on the server while the client sees an error. You need retries, timeouts, idempotency, and circuit breakers.
- Data consistency is harder. A single business operation often spans several services. You can no longer rely on one database transaction. You design around eventual consistency, compensating actions, or the saga pattern.
- Observability is mandatory. Without distributed tracing, correlated logs, and metrics per service, a single failed request becomes a search across ten log streams.
- Operations multiply. Each service needs build, deploy, configuration, secrets, dashboards, alerts, and an owner. Ten services is ten things to keep healthy.
- Local development gets harder. Running the whole system on a laptop means running many containers, their databases, message brokers, and gateways, all wired together correctly.
- Versioning and contracts matter. Changing an API now affects every consumer. You need compatibility rules, schema validation, and deprecation paths.
- Testing shifts. Unit tests still matter, but end-to-end tests across services are slow and fragile. Contract tests and well-scoped integration tests carry more weight.
Core design principles
Split by business capability
Start from the domain, not from the technology. Useful boundaries come from the language your business already uses: orders, payments, shipping, catalog, identity. Domain-driven design calls these bounded contexts: areas where a term has one consistent meaning.
A warning sign: if every feature request touches four services, your boundaries are in the wrong place.
Own your data
Each service should own its data and expose it only through its API. Sharing a database between services couples their schemas, their deployments, and their failure modes. It is the most common way a "microservice" system quietly becomes a distributed monolith.
Design APIs as contracts
Treat every interface as a public contract:
- Version it explicitly (
/v1/, or a schema version field). - Make changes backward compatible where possible: add fields, do not rename or remove them.
- Validate payloads and document them with OpenAPI, Protocol Buffers, or AsyncAPI.
- Give clients clear error semantics so they know whether a retry is safe.
Prefer asynchronous communication where it fits
Synchronous calls (HTTP or gRPC) are simple and fit request-response work, such as "fetch the user's cart." Asynchronous messages (events over a broker such as NATS, Kafka, or RabbitMQ) fit work that does not need an immediate answer, such as "an order was placed, so update analytics and send a confirmation email."
Events decouple services in time and in dependency. They also make the system harder to reason about, because the flow is spread across handlers. Use them deliberately and document the event flows.
Make every call safe to repeat
Networks retry. Clients retry. Users double-click. Operations that create, charge, or send must be idempotent: running them twice has the same effect as running them once. Use idempotency keys, upserts with natural keys, or deduplication tables.
Plan for failure
Assume every dependency will fail. Practical patterns include:
- Timeouts on every outbound call. A missing timeout is a latent outage.
- Retries with exponential backoff and jitter, limited in number, and only for safe operations.
- Circuit breakers that stop calling a failing dependency and fail fast.
- Bulkheads that isolate resource pools so one slow dependency cannot exhaust all threads or connections.
- Graceful degradation. If recommendations are down, show the page without them.
Data consistency patterns
Since each service owns its data, cross-service workflows need explicit strategies.
- Saga. A sequence of local transactions, each with a compensating action. If "charge card" succeeds but "reserve stock" fails, the saga refunds the charge. Sagas come in two flavors: choreography (services react to each other's events) and orchestration (a coordinator service drives the steps). Orchestration is easier to follow; choreography is more decoupled.
- Transactional outbox. A service writes the business change and an event record in one local transaction, and a separate process publishes the event. This avoids the classic bug where the database commit succeeds but the event publish fails, or the reverse.
- Event sourcing. Store state as a sequence of events and derive current state from them. Powerful for audit trails and replay, but significantly more complex. Do not adopt it by default.
- CQRS. Separate the write model from read models optimized for queries. Useful for read-heavy views that combine data from several services. It adds lag between a write and its visibility in reads.
Most systems need the outbox and a saga or two. Event sourcing and CQRS are situational.
Service discovery, gateways, and routing
- Service discovery. Services need to find each other. In Kubernetes this is built in through DNS and Services. In a local or VM-based setup, you need configuration or a discovery tool.
- API gateway. A single entry point that handles routing, authentication, rate limiting, and TLS termination for external clients. It keeps internal topology private.
- Service mesh. A layer (such as Istio or Linkerd) that handles mTLS, retries, and telemetry between services without application code changes. Powerful, and an operational commitment in its own right.
Start with the simplest working option. A gateway such as Traefik or NGINX, plus clear service addresses, covers most teams for a long time.
Observability
You cannot operate what you cannot see. A production-ready microservice system needs:
- Structured logs with a shared correlation or trace ID propagated across every call.
- Metrics per service: request rate, error rate, latency percentiles (RED method), and resource usage.
- Distributed tracing with OpenTelemetry, so one user request shows as a tree of spans across services.
- Health endpoints that distinguish liveness (the process is alive) from readiness (it can serve traffic).
- Alerts on symptoms users feel, such as error rate and latency, not only on internal resource counts.
Set this up before the second service, not after the tenth.
Security
- Authenticate and authorize at the edge and between services. Use short-lived tokens (for example OAuth 2.0 / OIDC access tokens) and mutual TLS or workload identity for service-to-service calls.
- Apply least privilege. Each service gets only the database credentials, queues, and permissions it needs.
- Manage secrets properly. Use a secrets manager or your platform's secret store. Never bake secrets into images or commit them to repositories.
- Scan dependencies and images. More services means more supply chain to watch.
- Validate input at every boundary. A service should not trust that its caller already validated the data.
Deployment and operations
- Containerize every service. A container image is the standard unit of deployment for microservices.
- Automate build, test, and deploy per service. Each service should have its own pipeline that can ship independently.
- Use progressive delivery. Canary releases, blue/green deployments, and feature flags reduce the blast radius of a bad release.
- Keep configuration external. Environment variables or config services let the same image run in every environment.
- Choose an orchestrator deliberately. Kubernetes is the common production choice, managed through Helm, Kustomize, or Argo CD. Smaller systems can run on managed container platforms that hide most of the cluster work.
- Plan ownership. Every service needs a named owner, a runbook, and an on-call path.
Local development: the part teams underestimate
Microservices make the laptop harder. A feature may touch a gateway, three services, two databases, a message broker, and an identity provider. Developers who must hand-write Compose files, reverse-proxy configs, and Tilt files for each change lose time and make mistakes. Teams either stop running the full system locally, or they run it and spend their focus on plumbing.
The goal of local development is a stack that behaves like production in its shape (services, routing, databases, brokers) while staying fast to change. A good local setup should:
- start the whole system with one command,
- rebuild and reload a service when its code changes,
- route traffic through the same gateway pattern used in production,
- make generated configuration easy to inspect and check for drift,
- avoid requiring a local Kubernetes cluster for everyday work.
How TDK CLI approaches it
TDK CLI is built for this local layer. You describe each service in a service.json file, and TDK generates the local runtime files from it: the Dockerfile, Traefik routing, and the Tiltfile. You do not hand-write a docker-compose.yml or nginx.conf.
A typical flow looks like this:
- Give each service its own directory with a
service.jsonthat describes its runtime, port, and dependencies. - Run
tdk up. Docker runs the containers, Traefik routes traffic between them, and Tilt watches the services and live-updates containers as you edit code. - Inspect what is running with
tdk ui, an interactive terminal dashboard, or check the generated files for drift when you change configuration.
Two boundaries are worth knowing. TDK CLI is for running services on your machine; it is not a deployment tool. Production deployment stays with Helm, Argo CD, or Kustomize. And a local stack is for development and testing, not a substitute for staging or load testing.
Local tooling does not replace good architecture. It removes the friction that makes an already-good architecture painful to work with day to day.
Testing microservices
A practical testing pyramid for microservices:
- Unit tests for business logic inside each service. Fast, many, and the majority of your suite.
- Contract tests (for example with Pact) that verify a consumer and a provider agree on the shape of an interface. These catch breaking changes without deploying everything together.
- Component tests that run one service with its real database and stubs for its dependencies.
- A small set of end-to-end tests for critical user journeys. Keep these few, because they are slow and fail for reasons unrelated to your change.
Run contract tests in CI for every service. A failing contract is a far cheaper signal than a production incident.
Common anti-patterns
- The distributed monolith. Services that must be deployed together, share a database, or call each other in long synchronous chains. You get the costs of microservices with none of the benefits.
- Nano-services. Services so small that every feature requires changes to five of them.
- Shared libraries with business logic. A "common" library that every service depends on becomes a hidden coupling point. Share only stable, generic utilities.
- Chatty synchronous chains. A request that hops through six services in sequence multiplies latency and failure probability.
- No timeouts. One stuck dependency takes every caller down with it.
- Skipping observability. Discovering that you cannot debug the system during the first real outage.
- Splitting too early. Creating services before you understand the domain. Boundaries drawn wrong are expensive to redraw.
When not to use microservices
Microservices are not the default. Think twice if:
- your team is small (a handful of engineers can usually ship a monolith faster),
- your domain is not yet well understood,
- you have no automated deployment, monitoring, or on-call practice,
- your scaling and team problems are solved by modularizing a monolith.
A modular monolith gives you clean boundaries, separate modules with explicit interfaces, and a single deployable unit. Many teams should start there and extract services later, one at a time, when a real need appears. Extracting a well-bounded module is much easier than undoing a poorly bounded service.
A checklist before you split
- Can you name each service's business capability in one sentence?
- Does each service own its data, with no shared tables?
- Is every synchronous call guarded by a timeout, and every retry safe?
- Do you have a plan for cross-service consistency (outbox, saga, or both)?
- Can you trace one request across all services?
- Can a team deploy its service without asking anyone else?
- Can a developer run the relevant services locally with one command?
- Does someone own each service and its runbook?
If most answers are yes, microservices are likely a good fit. If several are no, fix those first, or stay with a modular monolith a while longer.
Summary
Microservices give large organizations independent teams, independent deployments, and targeted scaling. They also bring distributed-systems problems into every feature: unreliable networks, split data, harder debugging, and more operations. Design around business capabilities, own your data, make interfaces explicit, plan for failure, invest in observability, and keep local development fast. Start with the simplest architecture that solves your current problem, and split only when the organization or the workload truly demands it.
Top comments (2)
links github.com/tdk-landscape/awesome-t... .
microservice core github.com/tdk-landscape/tdk-cli-core .