Riadh Mnasri
← Back to blog
5 min read

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

ObjectRoleWhat specifically concerns a Java developer
PodDeployment unit, one or more containersUsually a single JVM container per pod, sometimes a sidecar (monitoring agent, proxy)
DeploymentManages replicas and rolling updatesJVM startup time determines how fast a rolling update actually goes
ServiceExposes a set of pods under a stable IP/nameDecoupled from pod lifecycle, including during a JVM restart
ConfigMap / SecretExternalized configuration and secretsDirectly competes with Spring profiles (application-{profile}.yml)
IngressExternal HTTP routing to ServicesThe 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

AspectGKE (GCP)AKS (Azure)EKS (AWS)
Pod → cloud service identityWorkload IdentityMicrosoft Entra Workload IDIAM Roles for Service Accounts (IRSA), or EKS Pod Identity
Managed container registryArtifact RegistryAzure Container RegistryElastic Container Registry
Node autoscalingAutopilot (managed) or classic node poolsCluster AutoscalerCluster Autoscaler or Karpenter
Default loggingCloud Logging, agentlessContainer 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.