Kubernetes for Backend Developers: What Actually Matters
- kubernetes
- devops
- java
- backend
You don't need to know how the scheduler works. You need to know what breaks at 3 AM.
I've spent years working with Kubernetes as a backend developer — not a platform engineer. My job is to ship services that run reliably, not to manage cluster infrastructure. Here's what actually matters for that job.
The Mental Model That Matters
Pods are ephemeral. They die and are replaced. Never treat a pod like a server. No SSH-ing in to make changes, no relying on local disk state, no assumptions about the pod IP being stable.
Services are stable. A Service in Kubernetes is a stable DNS name and virtual IP that load-balances to whichever pods are currently healthy and running. Your application connects to http://order-service:8080 and Kubernetes handles routing to the right pod.
Deployments manage rollout. A Deployment describes your desired state — 3 replicas of version 1.4.2. Kubernetes continuously reconciles reality toward that desired state. If a pod dies, a new one starts. If you push a new image, pods are replaced one by one.
That's the core mental model. Everything else is detail.
Resources and Limits — Non-Negotiable
Every Spring Boot application you deploy to Kubernetes must have resource requests and limits set. Without them, your pod is an uncontrolled tenant — it can consume all memory on a node, triggering OOMKill of other pods, and the scheduler has no information for placement decisions.
The most common production incident I've seen: OOMKilled in pod status. The pod starts fine, runs for a few hours under load, then the JVM heap grows past the container memory limit and the kernel kills the process.
The fix is two-part. First, set sensible limits:
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "768Mi"
cpu: "1000m"
Second, tell the JVM to respect the container's memory limit instead of trying to use 25% of the node's total RAM:
-XX:MaxRAMPercentage=75
With a 768Mi limit and MaxRAMPercentage=75, the JVM heap ceiling is approximately 576Mi. Leave some headroom for non-heap memory (Metaspace, thread stacks, off-heap buffers).
Health Probes — Get These Right
Kubernetes uses probes to decide whether to send traffic to your pod and whether to restart it. Getting them wrong is how you end up with a Deployment that loops restarts or routes traffic to pods that aren't ready.
Liveness probe: Is this pod still alive and worth keeping? If it fails, Kubernetes restarts the pod. Wire it to an endpoint that confirms the application process is running and not deadlocked — not an endpoint that checks database connectivity. If the DB is down, you don't want your pods restarting in a loop.
Readiness probe: Is this pod ready to receive traffic? If it fails, the pod is removed from the Service's endpoint list but not restarted. Wire it to an endpoint that checks dependencies — DB connection, downstream services.
Spring Boot Actuator gives you both out of the box:
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20
periodSeconds: 5
failureThreshold: 3
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
failureThreshold: 30
periodSeconds: 5
The startup probe is important for JVMs. A Spring Boot application with a moderate classpath can take 20–40 seconds to start. Without a startup probe, the liveness probe starts firing before the app is ready, Kubernetes restarts it, and you get an infinite restart loop.
In application.properties:
management.health.livenessstate.enabled=true
management.health.readinessstate.enabled=true
ConfigMaps and Secrets
The cleanest pattern is environment injection — mount your ConfigMap and Secret values as environment variables. Spring Boot picks them up via standard property binding.
envFrom:
- configMapRef:
name: myapp-config
- secretRef:
name: myapp-secrets
For Secrets that change without a pod restart (certificate rotation, API key rotation), volume mounts are more useful — Spring Cloud Kubernetes can watch the mounted file and refresh properties without a restart.
Don't put Secrets in your source code or your values.yaml in Git. Use a Secrets operator (External Secrets Operator with AWS Secrets Manager or HashiCorp Vault) if your organisation has one.
Rolling Updates and Traffic Gates
When you push a new version, Kubernetes performs a rolling update. Pods running the old version are replaced one by one with pods running the new version.
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
With maxUnavailable: 0, Kubernetes will never reduce capacity below the desired replica count during a rollout. New pods start and become ready before old pods are terminated. The readiness probe is what gates traffic — a new pod only receives requests once its readiness probe passes.
This is why readiness probes that check real dependencies matter. A pod that starts successfully but can't reach the database will fail its readiness probe and never receive traffic. Your rollout pauses with old pods still serving rather than routing traffic to a broken pod.
What to Actually Learn First
Forget the operator pattern and custom controllers. For day-to-day backend development, learn these kubectl commands and understand what they tell you:
# What is this pod doing right now?
kubectl describe pod <pod-name>
# What did it log?
kubectl logs <pod-name> --previous # previous container if pod restarted
# Is it running? Is it ready?
kubectl get pods -o wide
# What events happened to this deployment?
kubectl describe deployment <deployment-name>
# Run a shell in a pod for debugging
kubectl exec -it <pod-name> -- /bin/sh
The Events section of kubectl describe tells you why a pod failed to start, why it was OOMKilled, why it failed a health check. It's the first place to look.
A complete Deployment manifest for a Spring Boot application with everything above:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: registry.example.com/order-service:1.4.2
ports:
- containerPort: 8080
env:
- name: JAVA_OPTS
value: "-XX:MaxRAMPercentage=75"
envFrom:
- configMapRef:
name: order-service-config
- secretRef:
name: order-service-secrets
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "768Mi"
cpu: "1000m"
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
failureThreshold: 30
periodSeconds: 5
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 0
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 0
periodSeconds: 5
failureThreshold: 3
This is enough for 80% of your daily Kubernetes work. Master the probes, the resource limits, the rolling update strategy, and the five kubectl commands above. The rest is refinement.