Every microservice you ship drags the same invisible cargo: discovery, state, messaging, secrets, retries, telemetry. This walkthrough builds up from why cloud native exists, through containers and Kubernetes, to the sidecar runtime that lifts that cargo out of your code — with live demos you can poke along the way.
Start at the beginning ↓"Cloud native" isn't a technology, it's a checklist of expectations. A payments service, a storefront, a booking system — whatever the app — the business now assumes it will keep running through traffic spikes, hardware failures, and its own upgrades, without anyone noticing. That assumption didn't exist twenty years ago. A single server going down used to just mean a bad afternoon. Now it has to mean nothing at all.
Six properties make up that checklist, and each one pushes back on how software used to be built:
Grows with load and traffic — designed to scale as the business scales, not re-architected every time it does.
Withstands component failures, up to a full cloud-region outage, and stays functional throughout.
Adds and removes running instances automatically as demand rises and falls — capacity that breathes.
Runs 24×7×365. No downtime, even while it's being upgraded or patched underneath you.
Protects data and transactions against fraud and attack at every layer, not just the perimeter.
Services develop, deploy and release on their own cadence — aligned with agile teams working in parallel.
Think of a restaurant that only ever plans for its slowest Tuesday lunch. One food-blogger review and a queue forms out the door — no extra tables, no extra cooks, and the kitchen catches fire from a single failing burner because there's no backup ring. Cloud native is the restaurant that keeps a bank of foldable tables in the back, cross-trains every cook on every station, and can lose a burner without the dinner rush even noticing. The tables and cross-training cost something up front. That's the trade this whole article is really about.
Drag the target and watch how little slack each extra nine actually leaves.
The six demands above are the why. The practice that emerged to satisfy them has its own checklist — built on cloud infrastructure, split into microservices, shipped as containers, talking through contracts instead of shared code, coordinated by an orchestrator, released independently, spread across clouds, with total cost of ownership always in view. Every later section in this article is really an elaboration of one item on that list.
Elasticity — on a normal day, two instances of the storefront handle all traffic comfortably. On a sale day, two instances fall over — so the platform notices the queue building and adds instances automatically, then removes them once the peak passes. Nobody paged anyone.
Resiliency — a single checkout request touches the catalog, the cart, and the payment service. If payment goes down, a resilient storefront doesn't fail the whole request — it degrades gracefully, shows a clear "try again shortly" message on checkout, and keeps the catalog and cart online for everyone else.
Cloud native sets the demands; something has to actually run the software that meets them. That something went through three distinct eras, each solving the isolation problem the previous one left half-finished.
Traditional deployment is one big house with no interior walls — one messy tenant's clutter spills into everyone else's room. Virtualization builds separate houses, each with its own foundation, plumbing, and utilities, which is safe but expensive to duplicate. Containers are more like apartments in one building: separate units with locked doors (isolation), sharing the building's foundation and pipes (the host kernel) instead of each pouring their own concrete.
Move the instance count and watch the guest-OS tax on VMs grow while the container line stays flat.
Three words get used almost interchangeably and shouldn't be. A container is a lightweight, running package of app code plus its exact runtime and library dependencies — decoupled from the host so it behaves identically anywhere. An image is the immutable template that container is started from; a container is one running instance of an image, the same way an object is one instance of a class. Docker is the tooling that builds, ships, and runs both. A Java image, for example, stacks an OS layer, a JVM, your app, and its JARs — so docker run reproduces that exact environment on any host, laptop or cloud.
Containers don't run themselves in production. Once you have many instances of many services, someone has to keep answering four questions continuously: which healthy instance serves this request, is each instance actually healthy, how do we absorb a traffic burst, and how do services find each other by name instead of a fragile IP address. Kubernetes is the system that answers all four, permanently, without a human in the loop.
Click a pod to kill it. Watch Kubernetes notice and replace it — this is the "orchestrator" doing its job, not magic.
The smallest deployable unit — a single instance of a running process. Holds one or more containers sharing network and storage. Pods are ephemeral by design.
A stable abstraction over a set of Pods — one IP and DNS name, load-balanced across them. Pods come and go; the Service endpoint stays put.
Hybrid cloud is supposed to avoid lock-in by letting you fail over between providers — but each provider ships its own CLI, its own service discovery, its own event protocol, so deploying the same app everywhere automatically was effectively impossible. Standardizing on Kubernetes gives every provider the same portable deployment surface. Kubernetes-native isn't a departure from cloud-native — it's cloud-native with one concrete, common substrate underneath.
Kubernetes solves scheduling, health, scaling, and naming — for the container. It has no opinion on what's running inside that container. A real application is many services calling each other, and every one of them independently has to solve the same handful of distributed-systems problems: how do I find and call another service, how do I persist and share state, how do I send an async message, how do I fetch a secret, how do I retry a failing call without hammering it forever, how do I emit logs and traces anyone can actually read. Kubernetes runs the box. Nobody built what goes inside it for you.
Discover and call other services reliably.
Persist and share state across services.
Async messaging between services.
Fetch credentials from a secret store.
Connect to external systems & events.
Retries, timeouts, and circuit breaking.
Logs, metrics, and distributed traces.
Same seven, every service. The whole rest of this article is one idea: build these once, not per team.
Traditionally these seven ship as a framework library compiled straight into every service. The business logic ends up as a thin sliver wrapped in a thick shell of plumbing — the JVM (or Node runtime, or .NET runtime) hosts a framework library, and your actual code is a small guest inside its own container.
It's like every restaurant in a chain building its own walk-in fridge, its own point-of-sale system, and its own staff-scheduling software from scratch — in whatever "language" that location happens to speak. Some franchises end up with excellent fridges and terrible scheduling software, because one team is good at refrigeration and bad at scheduling. Nobody chose that; it's just what happens when every location re-solves every problem alone.
Set how many services and how many languages a fleet uses, and compare the two approaches head-to-head.
The SDK binds your code to one provider's API. Switching a queue, database, or secret store means rewriting and redeploying every service that touches it.
The same plumbing is embedded and version-pinned in every service. One upstream change forces coordinated updates across the whole fleet at once.
Each language needs its own port of the library. Java, Go, .NET, and Python teams re-solve the same problem in parallel, independently.
Dapr — Distributed Application Runtime — takes the seven building blocks out of your compiled code and runs them as a separate process, alongside your app, in the same Pod. Your app talks to that process over plain local HTTP or gRPC calls. It is a graduated CNCF project since 2024 — the same top tier as Kubernetes, Istio, and Prometheus — precisely because "run the plumbing next door instead of inside" turned out to generalize far beyond any one team's stack.
Not a library linked into your app — its own OS process, with its own lifecycle.
Talks to the app over a fast local call, not a remote network hop.
The exact same sidecar binary works next to Java, Go, Python, or Node — no per-language SDK to port.
A sidecar is like hiring a bilingual local fixer in a foreign city instead of teaching every visiting employee the language, the transit system, and who to bribe for a permit. The fixer rides alongside each visitor, handles the local specifics through one simple conversation ("get me a permit"), and can be swapped for a different fixer in a different city without the visitor changing how they ask.
Plumbing compiled into every service, per language.
Business logic only; plumbing delegated over a standard local API.
Your app calls one stable API. Which real backend answers that call is chosen entirely by a YAML file the sidecar reads at startup — a pluggable component. Swapping a backend means editing that file, not touching application code:
apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: statestore spec: type: state.redis # ← change this line to state.postgresql metadata: - name: redisHost value: localhost:6379
Your app's call never changes. Pick a different backend and watch only the YAML — and the arrow pointing at a different box — update.
Five of the seven building blocks are pluggable APIs you call directly — invocation, state, pub/sub, secrets, bindings. The other two, resiliency and observability, aren't APIs you call at all; Dapr applies them underneath every one of the other five, automatically, because every hop already passes through a sidecar that can retry it, time it out, and trace it.
One service calls another by a logical app-id, never an IP address. The call leaves the Employee sidecar, crosses to the Department sidecar over an encrypted mTLS connection, and lands on the Department app — automatic retries and full tracing included, with zero retry or TLS code written by either team.
Call by app-id, not IP.
Encryption between sidecars, by default.
Automatic, policy-driven, no code.
Every hop is observable end to end.
Services often need to remember something beyond a single request, and share it with other services. Dapr gives every language the same two calls — getState / setState — against whichever store is configured, with no store-specific SDK anywhere in the app.
Loosely coupled, asynchronous messaging — the publisher never knows or cares who subscribes. The broker (Kafka, RabbitMQ, MQTT, Redis Streams) is a YAML choice, so the messaging technology underneath can change without touching a single service.
Apps need credentials to reach the outside world, but embedding a cloud provider's secrets SDK in every service is exactly the lock-in this whole approach exists to avoid. One call handles it everywhere:
password = dapr.getSecret("db-password")
The vault behind that call is a YAML choice too — HashiCorp Vault, Azure Key Vault, AWS Secrets Manager, or GCP Secret Manager, picked per environment, never per line of app code.
Bindings connect to on-premises or cloud resources — SMTP, Twilio, cloud queues, cron, webhooks — without embedding any of their SDKs in every service. An input binding means an external event triggers your app; an output binding means your app invokes an external system. Either way, a change to the external connector touches configuration, not code, in every microservice that uses it.
A retry policy turns one unreliable call into several chances at a reliable outcome. This is declarative policy the sidecar enforces — retries, timeouts to bound how long any call may wait, and circuit breaking to stop calling a dependency that's clearly down until it recovers — none of it is conditional logic scattered through your services.
A flaky dependency that only succeeds 70% of the time on its own looks a lot better after a few automatic retries.
All traffic already flows through sidecars, so Dapr emits logs, metrics, and traces automatically, in the OpenTelemetry format — pointed at Zipkin, Jaeger, or Prometheus/Grafana. Nobody had to instrument a single service by hand for the sidecar's own hops to become visible.
Dapr runs in two places. Self-hosted mode runs the sidecar as a local process on your machine — the fast path for development — and can also run on VMs to serve real production workloads. Kubernetes mode injects the sidecar into every Pod automatically; it runs on any supported Kubernetes version, and includes minikube for local development and testing before anything touches a real cluster.
Two Spring Boot services — Employee and Department — with CRUD across both, each running its own Dapr sidecar, deployed together on Google Kubernetes Engine (GKE).
Press play and watch a single request cross from one service to another entirely through sidecars — no direct network code in either app.
Develop the Spring Boot Employee service → test it standalone → add the Dapr sidecar → containerize with Docker → deploy to minikube.
Develop Department → call it from Employee via Dapr → add its sidecar → containerize → deploy to minikube.
Every Dapr building-block call is the same local HTTP shape: talk to your own sidecar, name the block, name the target. Pick the pieces below and watch the real request assemble.
Discover and call other services by app-id.
getState / setState against any configured store.
Async messaging through a swappable broker.
One call, any vault, chosen in YAML.
External systems wired in through configuration.
Retries, timeouts, circuit breaking — applied throughout.
Logs, metrics, and traces, emitted automatically.
Invocation, state, pub/sub, secrets, and bindings are pluggable APIs. Resiliency and observability are cross-cutting concerns Dapr applies underneath all five, not APIs you call directly.
Your services shrink back down to business logic — the plumbing becomes portable across clouds and across languages, because it never lived inside your code in the first place.
Treating Dapr as a place to put business logic — it's a runtime you call, not a framework you build inside; your code should still run and mean something with the sidecar turned off. Forgetting that a component swap in YAML is still a production change: a new backend has its own latency and failure characteristics even though the calling code didn't move. And skipping observability because "the sidecar handles it" — it emits the telemetry, but someone still has to look at the dashboard.
Before next session: install the Dapr CLI and run dapr init in self-hosted mode, run the service-invocation and state quickstarts, and deploy the Employee & Department sample to minikube.
Copy a prompt to your AI tool to create your own version — go deeper on one section, adapt it to your stack, or remix it for a different runtime like Actor Model, gRPC, or WebAssembly.
Add your name and email so we know who's liking and commenting. We won't spam you, and this browser won't ask again.
Comments
Comments and likes aren't connected to a backend yet — open
config.jsand fill in your Supabase URL and anon key. They'll still work on this screen, but won't be saved or shown to other visitors until that's done.