Cloud-Native Principles
Cloud-native as a set of runtime properties, not a stack of tools — the six pillars, leaning on the platform, design-first contracts, and the twelve-factor app reframed by who owns each factor.
Before any pattern, the principles that justify them. “Cloud-native” is not a list of products you adopt; it is a set of runtime properties a system has — and every later choice in this book either earns or spends one of them.
Six pillars
A cloud-native service exhibits six runtime properties — and the platform exists to make each of them the default:
Read each as a behaviour you design toward, not a product you install:
- Disposable — twelve-factor: fast start, clean stop, no local state, so the platform can move or restart it at will.
- Observable — it emits traces, metrics, and logs, and exposes health probes.
- Resilient — timeouts, retries, circuit breakers, graceful degradation.
- Elastic — it scales to load, and to zero.
- Automatable — declarative config, GitOps, self-healing.
- Loosely coupled — contracts not calls, async where it fits.
These six are the rubric for the rest of the book. Hold each design decision up to them.
Lean on the platform
The most important move is to lean on the platform instead of reimplementing it. Don’t write your own retry library when the mesh does it at the wire; don’t build a bespoke autoscaler when KEDA scales on lag and request rate; don’t hand-roll metrics plumbing when OpenTelemetry and the LGTM stack give it to you. Every line of resilience code you write per service is a line you maintain forever and that drifts between teams. The platform makes the good behaviour the default; your job is to stay out of its way and use it.
Design-first: the contract precedes the code
Design-first is the cultural prerequisite for everything that follows. The
contract — OpenAPI, .proto, AsyncAPI — is agreed and registered before code
is written, owned by the producing domain, and gated in CI. Consumers code
against the contract and its generated stubs, never against your database. That
is what actually delivers independent deployability: the contract is the
service boundary. Skip it and you get distributed coupling — the worst of both
worlds.
The twelve-factor app, revisited
The twelve-factor app is the 2011 Heroku methodology that first codified what we now call cloud-native. It predates Kubernetes yet maps onto it almost perfectly. Here are all twelve, one line each.
The useful way to read it today is not to recite all twelve, but to split them by who owns each one — the few you still write, versus the many the platform now provides for free.
The left column is your job; the right column is the platform’s. The whole point of a cloud-native service is to do the left column well and trust the right column — which is exactly what this code does.
Twelve factors, mapped to our system
Grouped by what each factor asks of you, the twelve land on concrete choices in the running example — and most resolve to a platform default rather than code you write.
Five groups cover all twelve. Build & ship (codebase, dependencies, build/release/run, parity) becomes one repo and one image per release. Configure (config) is read from the environment. Run on the platform (stateless processes, concurrency, disposability, port binding) is the runtime’s job — KEDA scales it, Istio fronts it, the platform disposes of it. Attach resources (backing services) makes Postgres, Kafka, and the schema registry swappable URLs, never libraries compiled in. And operate (logs, admin processes) pushes telemetry and one-off tasks off the request hot path, into OpenTelemetry and Kubernetes Jobs. The section tags on each row point to where that group is treated in depth — most of the twelve are now adopted, not authored.
A twelve-factor service, ready for the platform
Two factors dominate the code you actually write: config from the environment (factor III) and health and readiness probes the platform calls. Here it is in each stack — pick your tab.
# application.yml — config from the environment (factor III)
spring:
application:
name: order-service # surfaces in metrics, traces
kafka:
bootstrap-servers: ${KAFKA_BOOTSTRAP} # injected at runtime
management: # Actuator publishes probes
endpoint:
health:
probes:
enabled: true # /actuator/health/liveness
health: # /actuator/health/readiness
livenessstate.enabled: true
readinessstate.enabled: true
// Custom readiness contributor — auto-discovered as a HealthIndicator bean
@Component
public class DatabaseReady implements HealthIndicator {
private final JdbcTemplate jdbc;
public DatabaseReady(JdbcTemplate jdbc) { this.jdbc = jdbc; }
@Override public Health health() {
try { jdbc.queryForObject("SELECT 1", Integer.class); return Health.up().build(); }
catch (DataAccessException e) { return Health.down(e).build(); }
}
}
# application.properties — config from the environment (factor III)
quarkus.application.name=order-service
quarkus.application.version=1.4.0
%prod.kafka.bootstrap.servers=${KAFKA_BOOTSTRAP}
// SmallRye Health publishes the probes Kubernetes calls
@Liveness // /q/health/live — process is up
public class Alive implements HealthCheck {
public HealthCheckResponse call() { return HealthCheckResponse.up("alive"); }
}
@Readiness // /q/health/ready — deps reachable
public class Ready implements HealthCheck {
@Inject PgPool db; // injected datastore client
public HealthCheckResponse call() {
return db.query("SELECT 1") != null ? up("ready") : down("ready"); }
}
// Program.cs — minimal hosting, env-driven config (factor III)
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddEnvironmentVariables(); // overrides appsettings.json
// Health checks: liveness vs readiness, just like the WCF heartbeat —
// only Kubernetes calls them now, not your monitoring server.
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" })
.AddNpgSql(builder.Configuration.GetConnectionString("Db")!,
name: "db", tags: new[] { "ready" }); // AspNetCore.HealthChecks
var app = builder.Build();
// The cloud-native equivalent of IIS "Ping" + ARR health probes,
// but baked into the app, not the host.
app.MapHealthChecks("/healthz/live", new() { Predicate = c => c.Tags.Contains("live") });
app.MapHealthChecks("/healthz/ready", new() { Predicate = c => c.Tags.Contains("ready") });
app.Run();
# pyproject.toml (Poetry) pins everything; config comes from the environment
from fastapi import FastAPI
from pydantic_settings import BaseSettings
class Settings(BaseSettings): # config from env — factor III
database_url: str
kafka_bootstrap: str
settings = Settings()
app = FastAPI(title="order-service", version="1.4.0")
@app.get("/healthz") # liveness — process is up
def healthz(): return {"status": "ok"}
@app.get("/readyz") # readiness — deps are reachable
async def readyz():
await check_db(); await check_kafka()
return {"status": "ready"}
// conanfile.py pins everything; config comes from the environment
#include <drogon/drogon.h>
#include <cstdlib>
struct Settings { // config from env — factor III
std::string database_url = std::getenv("DATABASE_URL");
std::string kafka_bootstrap = std::getenv("KAFKA_BOOTSTRAP");
};
const Settings settings; // parsed once, immutable
constinit std::string_view kVersion = "1.4.0";
int main() {
auto& app = drogon::app();
app.registerHandler("/healthz", [](auto&&, auto&& cb){ // liveness — process is up
cb(drogon::HttpResponse::newHttpJsonResponse({{"status","ok"}}));
});
app.registerHandler("/readyz", [](auto&&, auto&& cb){ // readiness — deps reachable
check_db(); check_kafka();
cb(drogon::HttpResponse::newHttpJsonResponse({{"status","ready"}}));
});
app.addListener("0.0.0.0", 8080).run(); // factor VII · port binding
}
// main.go — config from the environment (factor III); two probes, not one
type Settings struct {
DatabaseURL string // DATABASE_URL
KafkaBootstrap string // KAFKA_BOOTSTRAP
}
func load() Settings { // config from env, never a baked-in file
return Settings{
DatabaseURL: os.Getenv("DATABASE_URL"),
KafkaBootstrap: os.Getenv("KAFKA_BOOTSTRAP"),
}
}
func main() {
cfg := load()
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) // liveness
})
mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) {
// readiness drains traffic on failure; it never restarts the pod
if err := errors.Join(checkDB(r.Context()), checkKafka(r.Context())); err != nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"status": "down"})
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "ready"})
})
slog.Info("starting", "version", "1.4.0", "kafka", cfg.KafkaBootstrap)
_ = http.ListenAndServe(":8080", mux) // factor VII · port binding
}
How the code works
The same two ideas appear in every tab:
- Config comes from the environment, never from a baked-in file. Spring and
Quarkus read
${KAFKA_BOOTSTRAP}from the environment, .NET callsAddEnvironmentVariables()so env vars overrideappsettings.json, Python binds aBaseSettingsclass to env vars, and C++ readsstd::getenv. One image, many environments — factor III, and the thing that lets the same artifact run in dev, staging, and production unchanged. - Two probes, not one. Liveness answers “is the process up?” — if it fails,
the platform restarts the pod. Readiness answers “are my dependencies
reachable?” — if it fails, the platform stops routing traffic to this instance
but leaves it running. Conflating the two is a classic mistake: a readiness
blip should drain traffic, not trigger a restart loop. The frameworks differ
in surface — Actuator endpoints, SmallRye
@Liveness/@Readiness, ASP.NET tag-filtered health checks, plain handlers — but Kubernetes calls them the same way.
Notice what isn’t here: no retry logic, no scaling code, no port-binding plumbing. Those are the right column of Figure 1.3 — the platform’s job.
Cross-check it yourself
With the service running, the probes are just HTTP. Hit liveness and readiness
with curl and confirm liveness returns healthy while the database is down but
readiness does not — that difference is the whole point. curl -i
localhost:8080/readyz should flip from ready to not-ready as you stop and start
the database, without the process ever restarting.
The code is in examples/01-cloud-native-principles/. The run script there builds
and runs it; its README.md covers what it does and how to drive it.
Verification status: verified — examples/01-cloud-native-principles/ passes 10/10 checks in each of the six languages on Docker Engine (2026-10-10)
(health probes, config from environment, CRUD, readiness flip on dependency failure).