What Are Microservices?

For decades the monolithic architecture was the default choice for building applications. It has since been displaced, for a large class of systems, by microservices. This page covers what microservices are, how they communicate, what makes them hard, and how Valkey and Redis are used to solve the shared-state problems that microservices create.

What is a microservice?

A microservices architecture is a software model in which a single application is composed of multiple smaller, loosely coupled services. Each service owns a highly specific function and can be developed, deployed, scaled, and failed independently of the others.

Microservices are usually contrasted with the monolithic architecture, in which all of these functions are combined into one deployable application. Monoliths have real advantages: simpler development and testing, easier debugging, and better performance from sharing a single process and address space. Their disadvantage is coupling. Because components share a deployment and often a database, a change to one part can force a redeploy of everything, and one component's resource demands can constrain the whole system.

Microservices are loosely coupled by design, which trades that coupling for flexibility and independent scalability — and for a new set of distributed-systems problems that did not exist in the monolith. Each service can be scaled up or down on its own, but now every call between services can fail, time out, or arrive twice.

The term is often used alongside service-oriented architecture (SOA). Both decompose a monolith into service modules, but microservices are generally considered an evolution of SOA: the services are smaller, own their own data, and are capable of functioning independently rather than depending on a central integration bus.

Major technology companies including Netflix, Amazon, eBay, and Apple have adopted the model at scale. Netflix, for example, migrated from a monolithic architecture to a service-oriented one to handle more than a billion calls per day, which greatly improved its ability to scale during demand peaks.

How do microservices work?

Microservices can be built from scratch or extracted from an existing monolith. Decomposition is usually iterative: a process is divided repeatedly until it reaches a set of fine-grained capabilities that map to individual services.

For example, an e-commerce application may have these high-level functions:

  • Inventory management
  • Catalog management
  • Order management and fulfillment
  • Product recommendations
  • Customer service

Each of these breaks down further. Inventory management, for instance, needs a service that decrements stock levels once a customer places an order — which immediately raises the question of what happens when two orders for the last item arrive at the same moment.

Microservices are commonly deployed in containers, which package an application's source code with its runtime environment: dependencies, libraries, and settings. Containers let each service be deployed independently while still running in a consistent environment, which is why they pair naturally with the model.

How microservices communicate

Once an application is split apart, the interesting design work moves to the spaces between the services. There are two broad approaches.

Synchronous communication. One service calls another directly over HTTP/REST or gRPC and waits for a response. This is simple to reason about and easy to debug, and it is the right choice when the caller genuinely needs an answer before it can continue. The cost is coupling in time: if the callee is slow or down, the caller is slow or down too. Chains of synchronous calls are how a single failing service takes an entire system with it.

Asynchronous communication. Services exchange messages through a message broker instead of calling each other. The sender publishes and moves on; the receiver processes when it is able. This removes the temporal coupling — a service can be redeployed or briefly unavailable without failing its callers — at the cost of eventual consistency and harder debugging. Getting a message onto the bus reliably when the sending service commits is itself a problem, and the usual answer is the outbox pattern. Asynchronous messaging comes in two shapes: publish/subscribe, where every interested service receives a copy of each message, and queues, where each message is handled by exactly one worker.

Most production systems use both, applying synchronous calls to queries that need an immediate answer and asynchronous messaging to work that can happen slightly later.

Event-driven microservices

Event-driven microservices take asynchronous communication further. Rather than sending commands that instruct another service to do something, each service publishes events describing facts that have already happened — OrderPlaced, PaymentCaptured, InventoryReserved — and other services subscribe to the events they care about.

The difference is where the knowledge lives. In a command-driven system the order service must know that billing, inventory, and notifications all need to be called. In an event-driven system it only knows how to announce that an order was placed, and a new consumer can be added later without the publisher changing at all.

The trade-offs are real. Event-driven systems are eventually consistent, message ordering is not guaranteed unless you arrange for it, consumers must be idempotent because at-least-once delivery means duplicates, and no single service holds the end-to-end picture of a workflow, which makes tracing harder. Coordinating a multi-service transaction usually means the saga pattern — a sequence of local transactions with compensating actions — rather than a two-phase commit across services.

For a fuller treatment of the style, its patterns, and when not to use it, see event-driven architecture.

Shared state: the hard part of microservices

Splitting logic across services is the easy half. The difficulty is that certain state is inherently shared, and once services no longer share a process or a database, coordinating that state becomes an explicit engineering problem. The recurring cases:

  • Caching. A per-instance in-process cache stops working as soon as you run several instances behind a load balancer, because each one holds a different view of the data. Distributed caching gives every instance one shared, consistent cache.
  • Mutual exclusion. Two instances of the same service can process the last item in stock simultaneously. A local synchronized block only guards one JVM, so preventing this race condition requires a distributed lock that all instances respect.
  • Sessions. If user session data lives in a single server's memory, every request must be routed back to that server, and losing it logs the user out. Externalizing sessions makes application instances stateless and disposable — see web session and Spring Session.
  • Rate limiting. A quota of 100 requests per minute enforced independently by ten instances is a quota of 1,000. Enforcing it correctly requires a shared rate limiter.
  • Transactions. An operation spanning several services cannot rely on a database transaction. It needs a distributed transaction or, more commonly, a saga.

Microservices with Valkey and Redis

Valkey and Redis are in-memory data structure stores used to implement key-value databases, caches, and message brokers. Their role in a microservices architecture is usually to be the shared coordination layer described above rather than a system of record.

Two properties make them a good fit. First, latency: because coordination happens on the request path — checking a cache, acquiring a lock, reading a session — the shared layer has to be fast enough that it does not become the bottleneck the architecture was meant to remove. Second, breadth: a single deployment can serve as cache, lock manager, session store, rate limiter, and message broker at once, which avoids adding five separate systems to a stack that is already operationally complex.

An architecture that may run hundreds of service instances also needs the coordination layer to be scalable and highly available. Cluster partitions data across nodes for horizontal scale, and Sentinel provides monitoring and automatic failover.

Java microservices with Redisson

Valkey and Redis expose commands over the RESP wire protocol, so Java applications connect through a client library. Redisson maps that protocol onto familiar Java interfaces, so the distributed primitives look like the concurrent objects Java developers already use.

A distributed lock, so that only one service instance decrements stock at a time:

RLock lock = redisson.getLock("inventory:sku-42");
lock.lock();
try {
    // only one instance across the whole cluster runs this at a time
    inventory.decrement("sku-42");
} finally {
    lock.unlock();
}

A shared cache with per-entry expiration, visible to every instance of the service:

RMapCache<String, Product> catalog = redisson.getMapCache("catalog");
catalog.put("sku-42", product, 10, TimeUnit.MINUTES);

Product cached = catalog.get("sku-42");

Beyond these, Redisson provides distributed collections, locks and synchronizers, rate limiters, session stores for Tomcat and Spring Session, remote services for RPC between services, and messaging. Redisson PRO adds Reliable PubSub and Reliable Queue with acknowledgments, delivery limits, dead-letter destinations, and delayed delivery — broker-grade guarantees without running a separate broker.

For practical walkthroughs, see session management for Java microservices and distributed caching in Java.

Microservices: Frequently Asked Questions

What is the difference between microservices and a monolithic architecture?

A monolith packages all of an application's functionality into a single deployable unit that shares one process and usually one database. Microservices split that functionality into independently deployable services that own their own data and communicate over the network. Monoliths are simpler to build and operate; microservices scale and evolve independently at the cost of distributed-systems complexity.

What is the difference between microservices and event-driven architecture?

Microservices describe how an application is decomposed — into small, independently deployable services.Event-driven architecture describes how components communicate — by publishing and reacting to events rather than calling each other directly. They are orthogonal: you can build microservices that communicate synchronously over REST, and you can build an event-driven monolith. They are frequently combined because event-driven communication removes the temporal coupling that makes chains of synchronous microservice calls fragile.

How do microservices communicate with each other?

Either synchronously, through HTTP/REST or gRPC calls where the caller waits for a response, or asynchronously, through a message broker using publish/subscribe topics or queues. Most systems use both, reserving synchronous calls for queries that need an immediate answer.

Do microservices need a shared database?

Generally no — each service is expected to own its data, since a shared database recreates the coupling microservices are meant to remove. Services do, however, commonly share infrastructure for coordination: a cache, a lock manager, a session store, or a message broker. That is a different thing from sharing a system of record.

Why do microservices need a distributed lock?

Because language-level synchronization only covers a single process. When several instances of a service run concurrently, a synchronized block or a ReentrantLock guards nothing across instance boundaries. A distributed lock stored outside the application — in Valkey or Redis, for example — is visible to every instance and can enforce mutual exclusion across the whole deployment.

Is Redis good for microservices?

Redis and Valkey are widely used in microservices architectures, though usually as a coordination and caching layer rather than as each service's primary database. A single deployment can serve as distributed cache, lock manager, session store, rate limiter, and message broker, which keeps operational overhead down. Java applications typically access it through a client such as Redisson, which exposes these capabilities as standard Java interfaces.

Similar terms