Kubernetes day to day for a Java developer: what actually matters

Most Kubernetes introductions talk about Pods and Deployments as if it were just another deployment tool. For a Java developer, the real subject lies elsewhere: Kubernetes imposes constraints (memory limits, health probes, externalized configuration) that rub directly against how the JVM has always managed its own resources. That friction is what you need to understand to use it day to day, not the list of API objects.
The objects you actually touch, day to day
| Object | Role | What specifically concerns a Java developer |
|---|---|---|
| Pod | Deployment unit, one or more containers | Usually a single JVM container per pod, sometimes a sidecar (monitoring agent, proxy) |
| Deployment | Manages replicas and rolling updates | JVM startup time determines how fast a rolling update actually goes |
| Service | Exposes a set of pods under a stable IP/name | Decoupled from pod lifecycle, including during a JVM restart |
| ConfigMap / Secret | Externalized configuration and secrets | Directly competes with Spring profiles (application-{profile}.yml) |
| Ingress | External HTTP routing to Services | The Ingress controller differs by cloud (see below) |
The most classic trap: the JVM doesn't know it's in a container (or knows it poorly)
Before Java 10, the JVM read the host machine's memory and CPU count, not
the container's limits. On a pod capped at 512 Mi but running on a 16 Gi
node, the JVM would compute a default heap based on the 16 Gi, and the
container would get killed (OOMKilled) well before the JVM thought it was
under memory pressure.
Since Java 10, UseContainerSupport is enabled by default, with a backport
to Java 8 (available starting with 8u191, but actually enabled by default
from 8u251 onward): the JVM correctly reads cgroup limits. The trap hasn't
disappeared though, it just changed shape: a fixed -Xmx, copied from one
environment to another, doesn't adapt if the pod's memory limit changes
between dev, staging and production.
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
# Not this: fixed, doesn't adapt if the limit changes
-Xmx400m
# Rather this: proportional to the container's actual limit
-XX:MaxRAMPercentage=75.0
MaxRAMPercentage computes the heap as a percentage of the memory actually
allocated to the container, not the host machine: the same Docker artifact
behaves correctly whether the limit is 512 Mi in dev or 2 Gi in production,
with no image rebuild needed.
Health probes, not simple pings
Kubernetes distinguishes liveness (should the pod be restarted?) from
readiness (should the pod receive traffic?), a distinction many Java
developers ignore by wiring both probes to the same endpoint. Since Spring
Boot 2.3, Actuator natively exposes both notions separately (enabled by
default starting with Spring Boot 4; before that version, you must
explicitly set management.health.probes.enabled=true):
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20
The most costly confusion: wiring liveness to a healthcheck that verifies an external dependency (database, third-party service). If that dependency goes down, Kubernetes restarts the pod in a loop, making things worse instead of stabilizing them. Liveness should answer a single question, "is this JVM process stuck?", not "are all its dependencies healthy?": that is the job of readiness, which simply removes the pod from traffic without killing it.
Externalized configuration: ConfigMap vs Spring profiles
A ConfigMap mounted as a file can replace a Spring profile, but the two
mechanisms have different precedence rules, and combining them without
thinking creates configurations that silently drift between environments.
The most robust practice: keep Spring profiles for the configuration
structure (which keys exist), and use the ConfigMap only for values that
genuinely change per environment, mounted as environment variables rather
than a file, so that @Value and @ConfigurationProperties resolve them
the same way regardless of environment.
What actually changes between GCP, Azure and AWS
| Aspect | GKE (GCP) | AKS (Azure) | EKS (AWS) |
|---|---|---|---|
| Pod → cloud service identity | Workload Identity | Microsoft Entra Workload ID | IAM Roles for Service Accounts (IRSA), or EKS Pod Identity |
| Managed container registry | Artifact Registry | Azure Container Registry | Elastic Container Registry |
| Node autoscaling | Autopilot (managed) or classic node pools | Cluster Autoscaler | Cluster Autoscaler or Karpenter |
| Default logging | Cloud Logging, agentless | Container Insights (Log Analytics) | CloudWatch Container Insights |
What the three have in common, more important than their naming differences: the federated identity mechanism (a pod obtains a short-lived cloud token through its Kubernetes ServiceAccount, with no static key stored as a secret) is progressively replacing long-lived credentials across all three clouds. That's the part worth understanding well, not the product names, which change faster than the underlying concept.
The real hidden cost: JVM startup versus autoscaling
A Horizontal Pod Autoscaler that reacts to a load spike by adding replicas implicitly assumes those replicas become useful quickly. A typical Spring Boot JVM often takes several seconds to become ready (Spring context loading, pool connections, JIT warmup), considerably more than a lighter runtime. Under a sudden traffic spike, autoscaling reacts, but the new pods only absorb load after that delay, during which the existing pods alone absorb the overload. The common answers (native GraalVM images, predictive rather than reactive pre-scaling based on metrics, or simply more generous minimum replicas) each carry their own cost, but the problem itself deserves to be anticipated at design time, not discovered on the day of the first real spike.
What I don't recommend by default: Kubernetes everywhere
On MissionMatch, services run on AWS ECS, not EKS. That choice isn't a dodge: ECS removes the responsibility of operating the Kubernetes control plane, for a need that doesn't justify that extra complexity (a moderate number of services, with no multi-cloud requirement or need for a specific Kubernetes operator ecosystem). Kubernetes is justified when multi-cloud portability, the tooling ecosystem (service mesh, operators), or the actual scale of the number of services demands it, not as a default choice because it has become the industry's perceived standard.