What is a Microservices Architecture?
A microservices architecture decomposes an application into a collection of small, independently deployable services, each responsible for a specific business capability. A payment service handles payments. A user service handles user profiles. A notification service handles emails and push notifications. Each service has its own codebase, its own deployment pipeline, and -- ideally -- its own database.
This is a popular architectural pattern, but it is worth being direct: microservices introduce significant operational complexity. Understanding when they are the right tool -- and when they are not -- is as important as understanding how they work. Microservices are tightly linked to Docker and Kubernetes, which provide the containerisation and orchestration that makes running many independent services practical.
Monolith vs Microservices
A monolith packages all application functionality into a single deployable unit. A single process handles HTTP requests, runs business logic, and talks to one database. Monoliths are not a legacy pattern -- they are the appropriate architecture for a large number of applications. They are simpler to develop, test, deploy, and debug. You can call a function instead of making a network request. Transactions are local. You have one place to look when something breaks.
The challenges emerge at scale. When 50 engineers work on the same codebase, deployments slow down and changes in one area affect another. When one feature needs to scale to handle 10x the traffic, you have to scale the entire application. When you want to rewrite a problematic module, you are constrained by the language and framework the whole monolith uses.
Microservices address these specific pain points -- at the cost of distributing the complexity in a different way.
Service Communication
Services in a microservices architecture communicate over the network. This has fundamental implications that in-process function calls do not have: the network can fail, be slow, or return unexpected results. You must design for partial failure.
Synchronous communication (REST over HTTP, gRPC) is request-response: Service A calls Service B and waits for a response. It is simple to reason about but creates temporal coupling. If Service B is slow, Service A is slow. If Service B is unavailable, Service A must handle that failure gracefully.
Asynchronous communication (Kafka, RabbitMQ, SQS) decouples services in time. Service A publishes an event to a message queue; Service B consumes it independently. This improves resilience and throughput but requires you to think carefully about message ordering, idempotency, and how to debug flows that span multiple queues.
Here is a minimal example of a service publishing an event via Kafka in Python:
from kafka import KafkaProducer
import json
producer = KafkaProducer(
bootstrap_servers=['kafka:9092'],
value_serializer=lambda v: json.dumps(v).encode('utf-8')
)
def publish_payment_processed(payment_id: str, amount: float, user_id: str):
event = {
"event_type": "payment.processed",
"payment_id": payment_id,
"amount": amount,
"user_id": user_id,
}
producer.send('payments', value=event)
producer.flush()
The notification service would have a consumer reading from the same topic and sending confirmation emails -- completely decoupled from the payment service.
Service Discovery and the 12-Factor App
When services are running as containers across a Kubernetes cluster, hardcoded IP addresses are useless -- pods move. Service discovery solves this: services register themselves with a discovery mechanism (Kubernetes DNS, Consul, AWS Cloud Map) and find each other by name rather than address.
The 12-factor app methodology, originally from Heroku, describes principles for building services that work well in this kind of environment: store config in the environment, treat logs as event streams, back-backing services as attached resources, and keep processes stateless. These principles align closely with how Kubernetes and cloud platforms expect applications to behave.
When Microservices Are the Wrong Choice
Microservices require investment in infrastructure before they pay dividends. Each service needs its own CI/CD pipeline, logging pipeline, health monitoring, and deployment configuration. Distributed tracing becomes necessary to debug problems that span multiple services. Database-per-service means you lose ACID transactions across business operations and must implement eventual consistency patterns.
For a team of 2--5 engineers building a product from scratch, this overhead is rarely justified. The advice from experienced engineers -- including Martin Fowler, who helped popularise the pattern -- is to start with a well-structured monolith and extract services when a specific, concrete pain point makes the overhead worth it.
Microservices make sense when: independent teams need to deploy independently without coordinating releases; a specific component has dramatically different scaling requirements; you need to use different technology stacks for different capabilities; or your domain boundaries are clear enough to draw service lines without creating distributed monolith problems.
See the DevOps tools guide for how containerisation and orchestration fit into this architecture.
