Spring Cloud Kubernetes: Service Discovery, ConfigMaps & Scaling
Master Spring Cloud Kubernetes for service discovery, ConfigMaps, and scaling in production.
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
- ✓Java 17+
- ✓Spring Boot 3.x
- ✓Kubernetes cluster (minikube or cloud)
- ✓Basic understanding of Kubernetes concepts: pods, services, ConfigMaps, HPA
- Use Spring Cloud Kubernetes for native service discovery via Kubernetes API, not Eureka.
- Externalize config with ConfigMaps and Secrets; avoid @Value for dynamic updates.
- Autoscaling works best with custom metrics; HPA alone is not enough for burst traffic.
- Beware of stale caches: Spring Cloud Kubernetes caches secrets/ConfigMaps by default.
Imagine your app is a food truck at a huge festival. Spring Cloud Kubernetes is like a festival organizer that tells each truck where the others are parked (service discovery), gives them the daily specials board (ConfigMaps), and calls in extra trucks when lines get long (scaling). Without it, each truck would have to radio every other truck manually and guess how many cooks to hire.
If you're running Spring Boot on Kubernetes and still using Eureka for service discovery, you're doing it wrong. I've seen teams deploy a five-node Eureka cluster on K8s only to realize Kubernetes already has a built-in service registry. Spring Cloud Kubernetes bridges the gap, letting you leverage native K8s APIs for service discovery, configuration, and scaling. But here's the kicker: the official docs make it look simple. They're not wrong, but they leave out the landmines. I've debugged production outages where a ConfigMap update took 15 minutes to propagate because of caching defaults. I've watched HPA thrash because we didn't understand pod metrics latency. This article gives you the real story—what works, what doesn't, and how to avoid the 2 AM pages.
Why Spring Cloud Kubernetes?
Let's cut through the hype. If you're already on Kubernetes, you have a service registry: the Kubernetes API server. You have DNS-based service discovery built into every pod. You have ConfigMaps and Secrets for configuration. So why add Spring Cloud Kubernetes? Because it gives you a unified, Spring-friendly way to interact with these primitives. Instead of manually parsing environment variables or calling the K8s API yourself, you get annotations like @DiscoveryClient and @ConfigurationProperties that work seamlessly with ConfigMaps. It also provides a reload mechanism so your app can pick up config changes without a full restart. But—and this is a big but—it's not magic. The abstraction leaks. I've seen teams blindly add the dependency and assume it works like Spring Cloud Config Server with Git. It doesn't. You trade one set of complexities for another.
Service Discovery: Not Your Father's Eureka
Service discovery in K8s works via DNS. When you create a Service, K8s assigns a DNS name (e.g., my-service.namespace.svc.cluster.local). Pods can resolve this name to the service's cluster IP, which load-balances across ready pods. Spring Cloud Kubernetes wraps this with a DiscoveryClient implementation. You can inject @Autowired DiscoveryClient and call getInstances(). Under the hood, it queries the K8s API for endpoints. This is great for polyglot environments, but there's a catch: the default implementation caches the endpoint list for 30 seconds. If you have rapid scaling, you might route to a pod that's been terminated. I once debugged a payment service that occasionally returned 503s because the cache still pointed to a terminating pod. The fix: set spring.cloud.kubernetes.discovery.cache-watch-interval=5000 (5 seconds) and enable watch events. But be careful—too low and you hammer the API server.
ConfigMaps and Secrets: The Good, the Bad, the Stale
ConfigMaps are K8s objects that hold key-value pairs or entire config files. Spring Cloud Kubernetes can automatically populate your Environment from a ConfigMap. You can even map a ConfigMap to a @ConfigurationProperties class. Sounds perfect, right? Not quite. The default behavior is to watch for changes and update the Environment in real-time. But 'real-time' is relative. The watch uses a long-polling mechanism with a 30-second cache by default. And if your service account lacks 'watch' permission, it falls back to polling every 30 seconds. I've seen teams tear their hair out because config changes took minutes to propagate. The solution: ensure RBAC includes 'watch', reduce cacheMillis, and consider using the reload feature (spring.cloud.kubernetes.reload.enabled=true) with 'event' mode. But be warned: reloading the context can cause unexpected behavior if your beans are not designed for refresh.
Scaling with HPA and Custom Metrics
Horizontal Pod Autoscaler (HPA) is the standard way to scale your Spring Boot app on K8s. It adjusts the number of replicas based on CPU, memory, or custom metrics. But here's the hard truth: default CPU-based scaling is often too slow for burst traffic. I've seen a payment gateway get overwhelmed because HPA took 2 minutes to react to a spike. The issue is that metrics-server polls kubelet every 15 seconds, and HPA evaluates every 30 seconds by default. The total lag can be over a minute. For faster scaling, use custom metrics (e.g., request rate via Prometheus Adapter) and tune the HPA behavior with the 'behavior' field (available in K8s 1.18+). Also, set resource requests and limits correctly—without them, HPA won't work. And don't forget to set the '--horizontal-pod-autoscaler-sync-period' flag on the controller manager if you need sub-30-second evaluation.
What the Official Docs Won't Tell You
Let's get real about the gaps. First, the official docs emphasize that Spring Cloud Kubernetes makes your app 'Kubernetes-native'. They don't mention that it also makes your app dependent on the K8s API server being available. If your API server goes down, your service discovery and config updates break. I've seen this happen during a cluster upgrade. Second, the caching defaults are dangerous. The docs say 'config.cacheMillis=30000' is fine, but in a production incident, that 30-second cache caused a 5-minute propagation delay because the watch event was lost and the fallback polling interval was also 30 seconds. Third, the reload feature is marketed as 'zero-downtime config updates'. It's not. If you have beans that hold state (e.g., connection pools), refreshing the context can close those pools and recreate them, causing transient failures. Fourth, the Fabric8 client has a memory leak in some versions (I'm looking at you, 4.11.x) when watches disconnect and reconnect. The official client is better but still not perfect. Finally, the docs don't warn you about RBAC. The default service account in many clusters has no 'watch' permission, so ConfigMap updates silently fall back to polling. You'll think your app is watching, but it's not.
Best Practices from the Trenches
After countless production incidents, here's my playbook for Spring Cloud Kubernetes. First, use the official Kubernetes Java client (spring-cloud-kubernetes-client) instead of Fabric8. It's lighter, better maintained, and has fewer memory issues. Second, set cacheMillis to 5000 for both config and discovery. If you're worried about API server load, use a watch with a backoff—but don't rely on polling. Third, for critical configs, avoid the reload feature entirely. Instead, use a sidecar like Reloader that triggers a rolling restart when a ConfigMap changes. It's simpler and more predictable. Fourth, always set resource requests and limits on your pods. Without them, HPA won't work and your pods may be throttled. Fifth, monitor the Kubernetes API server metrics (especially request latency and error rate) because your app's health depends on it. Sixth, use readiness probes that check if the pod is actually serving traffic, not just if the JVM is alive. Finally, test your scaling behavior with load tests that simulate burst traffic. Tune the HPA behavior until you're comfortable with the response time.
The ConfigMap Cache That Killed Our Canary
- Always verify RBAC permissions for watch operations—missing watch forces polling.
- Never assume ConfigMap updates are instant; test with a canary and monitor propagation delay.
- Use spring.cloud.kubernetes.config.cacheMillis=5000 for faster updates in dev, but beware of API server load.
- Add a health indicator that exposes the cached resourceVersion for debugging.
- For critical configs, consider using a sidecar like Reloader to trigger pod restarts.
kubectl exec -it <pod> -- nslookup <service-name>kubectl get endpoints <service-name>| File | Command / Code | Purpose |
|---|---|---|
| pom.xml | Why Spring Cloud Kubernetes? | |
| DiscoveryController.java | @RestController | Service Discovery |
| application.properties | spring.cloud.kubernetes.config.enabled=true | ConfigMaps and Secrets |
| hpa.yaml | apiVersion: autoscaling/v2 | Scaling with HPA and Custom Metrics |
| deployment.yaml | apiVersion: apps/v1 | Best Practices from the Trenches |
Key takeaways
Interview Questions on This Topic
Explain how Spring Cloud Kubernetes implements service discovery. What are the caching implications?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
That's Spring Cloud. Mark it forged?
4 min read · try the examples if you haven't