Skip to content

Containers & Kubernetes — Architect-Level Interview Guide

Target: Senior Engineer · Engineering Lead · Pre-Architect Focus: Docker, Kubernetes, probes, secrets management, service mesh


Q: How do you containerize a Spring Boot application with Docker? What are best practices?

Why interviewers ask this: Image size, security, and layer caching directly impact build speed, deployment time, and attack surface.

Answer

Naive Dockerfile (avoid):

FROM openjdk:17
COPY target/app.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
Problems: 600MB+ image, full JDK in production, rebuilds everything on any change.

Best practice — multi-stage layered build:

# Stage 1: Extract Spring Boot layers
FROM eclipse-temurin:21-jre-alpine AS builder
WORKDIR /app
COPY target/app.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

# Stage 2: Production image
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app

# Run as non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# Copy layers — only app code changes frequently
COPY --from=builder /app/dependencies/          ./
COPY --from=builder /app/spring-boot-loader/    ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/           ./

EXPOSE 8080

# Use exec form to receive signals correctly (graceful shutdown)
ENTRYPOINT ["java", \
    "-XX:MaxRAMPercentage=75.0", \
    "-XX:+UseContainerSupport", \
    "org.springframework.boot.loader.launch.JarLauncher"]

Why layers matter: Docker caches each layer. If only application code changes, the dependencies layer (unchanged) is reused — build goes from 3 min to 15 sec.

Layer rebuild frequency:

dependencies/        ← Rarely changes → cached aggressively
spring-boot-loader/  ← Rarely changes → cached
snapshot-dependencies/ ← Sometimes
application/         ← Every commit → always rebuilt

Image best practices:

Practice Why
Use alpine or distroless base Smaller attack surface, smaller image
Non-root user Limits damage if container is compromised
No JDK in production JRE only — eclipse-temurin:21-jre
Pin image tags eclipse-temurin:21.0.3_9-jre-alpine not latest
Scan image for CVEs Use docker scout, Trivy, or Snyk in CI
Set memory limits -XX:MaxRAMPercentage=75.0 respects container limits

Q: Describe the key components of a Kubernetes deployment for a Java microservice.

Answer

Complete production-ready deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: production
  labels:
    app: order-service
    version: "1.2.0"
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1           # Allow 1 extra pod during update
      maxUnavailable: 0     # Never reduce below desired count
  template:
    metadata:
      labels:
        app: order-service
    spec:
      serviceAccountName: order-service-sa   # Least privilege SA
      containers:

        - name: order-service
          image: myrepo/order-service:1.2.0
          ports:

            - containerPort: 8080
          env:

            - name: SPRING_PROFILES_ACTIVE
              value: "prod"

            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: order-db-secret
                  key: password
          resources:
            requests:
              memory: "256Mi"   # Guaranteed allocation
              cpu: "250m"
            limits:
              memory: "512Mi"   # OOM kill threshold
              cpu: "500m"
          startupProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            failureThreshold: 30
            periodSeconds: 2
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            periodSeconds: 10
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            periodSeconds: 5
            failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
  name: order-service
  namespace: production
spec:
  selector:
    app: order-service
  ports:

    - port: 80
      targetPort: 8080
  type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-service-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-service
  minReplicas: 3
  maxReplicas: 10
  metrics:

    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

Key components explained:

graph LR
    HPA["HPA · Auto-scale 3-10 pods"]
    Deploy["Deployment · 3 replicas"]
    Pod1["Pod 1"]
    Pod2["Pod 2"]
    Pod3["Pod 3"]
    Svc["Service · ClusterIP"]
    Secret["Secret · DB password"]
    SA["ServiceAccount · least-priv"]

    HPA -->|Scale| Deploy
    Deploy -->|Creates| Pod1
    Deploy -->|Creates| Pod2
    Deploy -->|Creates| Pod3
    Svc -->|Load balance| Pod1
    Svc -->|Load balance| Pod2
    Svc -->|Load balance| Pod3
    Secret -.->|Mounted| Pod1
    SA -.->|Identity| Deploy

Architect Insight

Always set both requests and limits. Without requests, the scheduler can't place pods correctly. Without limits, one runaway pod can starve other services on the node. Set memory limit = 2x request and cpu limit = 2x request as a starting point.


Q: How do you securely manage secrets in Kubernetes?

Why interviewers ask this: Secret sprawl is a compliance and security nightmare. Tests understanding of secret lifecycle management.

Answer

Kubernetes Secrets (built-in) — limitations:

  • Base64 encoded, not encrypted at rest by default
  • Visible to anyone with cluster access
  • No rotation, no audit trail

Better approaches:

1. Kubernetes + Sealed Secrets (GitOps-safe):

# Encrypt secret for Git storage
kubeseal --scope namespace-wide -o yaml < secret.yaml > sealed-secret.yaml
# Sealed secret committed to Git — only the controller in cluster can decrypt

2. HashiCorp Vault + Vault Agent Injector:

# Pod annotation — Vault injects secrets as files
metadata:
  annotations:
    vault.hashicorp.com/agent-inject: "true"
    vault.hashicorp.com/role: "order-service"
    vault.hashicorp.com/agent-inject-secret-db: "secret/data/order-service/db"
    vault.hashicorp.com/agent-inject-template-db: |
      {{- with secret "secret/data/order-service/db" -}}
      spring.datasource.password={{ .Data.data.password }}
      {{- end -}}

3. External Secrets Operator (AWS, GCP, Azure):

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: order-db-secret
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: order-db-secret   # Creates K8s Secret
  data:

    - secretKey: password
      remoteRef:
        key: prod/order-service/db
        property: password

Comparison:

Approach Encryption at rest Rotation Audit log Complexity
Plain K8s Secret ❌ (unless KMS) Manual Low
Sealed Secrets Manual Medium
Vault ✅ Auto High
External Secrets ✅ (cloud KMS) ✅ Auto ✅ (cloud) Medium

Common Mistake

Never use environment variables for secrets in Kubernetes. They're visible in kubectl describe pod, process listings, and crash dumps. Mount secrets as files with restrictive permissions (0400) and read them at startup.


Q: What are the best practices for container networking in microservices?

Answer

Network policy — default deny everything, allow explicitly:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: order-service-netpol
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: order-service
  policyTypes:

    - Ingress
    - Egress
  ingress:

    - from:
        - podSelector:
            matchLabels:
              app: api-gateway   # Only gateway can call order-service
      ports:

        - port: 8080
  egress:

    - to:
        - podSelector:
            matchLabels:
              app: inventory-service  # order-service can call inventory
      ports:

        - port: 8080
    - to:   # Allow DNS
        - namespaceSelector: {}
      ports:

        - port: 53
          protocol: UDP

Key networking concepts for microservices:

Concept Implementation Purpose
Service discovery Kubernetes DNS (order-service.production.svc) Find services by name
Load balancing Kubernetes Service (ClusterIP) Distribute traffic across pods
Ingress NGINX Ingress / Istio Gateway External traffic → cluster
mTLS Istio / Linkerd Encrypt and authenticate service-to-service
Network isolation NetworkPolicy Prevent lateral movement

Q: What is a service mesh? When should you use one?

Answer

A service mesh is an infrastructure layer that handles service-to-service communication, providing:

  • Traffic management — retries, circuit breaking, timeouts, traffic splitting
  • Observability — automatic distributed tracing, metrics per service pair
  • Security — mTLS between all services, certificate rotation
  • Policy enforcement — rate limiting, authorization policies

Without service mesh: Each microservice implements these itself (Resilience4j, Spring Cloud). With service mesh: Handled by sidecar proxy (Envoy) — zero code changes needed.

graph LR
    SvcA["Service A"]
    ProxyA["Envoy Sidecar"]
    SvcB["Service B"]
    ProxyB["Envoy Sidecar"]
    ControlPlane["Istio Control Plane · Istiod"]

    SvcA -->|localhost| ProxyA
    ProxyA -->|mTLS · with policy| ProxyB
    ProxyB -->|localhost| SvcB
    ControlPlane -.->|Config · certs| ProxyA
    ControlPlane -.->|Config · certs| ProxyB

When to use a service mesh:

✅ Use when:

  • You have 10+ services and maintaining resilience code in each is costly
  • You need zero-trust security (mTLS everywhere) as a compliance requirement
  • You need fine-grained traffic control (canary per service pair, fault injection for testing)
  • You want automatic observability without code instrumentation

❌ Skip when:

  • Small service count (< 10) — operational overhead outweighs benefits
  • Team unfamiliar with service mesh — steep learning curve (Istio especially)
  • Simple deployments — start with library-based resilience (Resilience4j) first

Cloud-Native Patterns


Q: When would you use serverless functions over containerised microservices? What are the trade-offs?

Why interviewers ask this: Serverless is widely adopted but often misapplied. Tests understanding of cold start penalties, statelessness constraints, and cost models.

Answer

Serverless (FaaS): Functions are deployed as individual units. The platform manages scaling, infrastructure, and lifecycle. You pay per invocation — not for idle capacity.

Dimension Serverless (Lambda · Azure Functions) Containerised Microservice (k8s)
Scaling Automatic, to zero Manual HPA config, min replicas > 0
Cold start 100ms–2s latency on first invocation No cold start (always-on)
Cost Pay per invocation (cheap for sporadic) Pay per running instance (wasteful if idle)
Execution limit 15 min max (AWS Lambda) Unlimited
State Stateless only Can be stateful with volumes
Vendor lock-in High (platform-specific runtimes) Low (portable OCI images)
Observability Platform-provided, less control Full control with custom tooling

Good serverless candidates:

✅ Use serverless for:
  - Event-driven processing: S3 upload triggers image resize
  - Scheduled jobs: nightly report generation, data cleanup
  - Sporadic webhooks: payment provider callbacks
  - Lightweight data transformations in data pipelines
  - Glue code between managed services

❌ Avoid serverless for:
  - Low-latency user-facing APIs (cold start is unacceptable)
  - Long-running workloads (>15 min processing)
  - Stateful services (session, in-memory cache)
  - High-throughput consistent traffic (containers are cheaper at scale)

Spring Cloud Function — portable serverless:

// Same function runs on AWS Lambda, Azure Functions, or as a Spring Boot endpoint
@SpringBootApplication
public class OrderProcessorApplication {

    @Bean
    public Function<OrderEvent, ProcessedOrder> processOrder() {
        return event -> {
            // Stateless transformation
            return orderProcessor.process(event);
        };
    }
}
graph LR
    S3["S3 Upload\nEvent"]
    Func["Lambda Function\nImage Resize"]
    DDB["DynamoDB\nStore Metadata"]
    SNS["SNS Topic\nNotify User"]

    S3 -->|Trigger| Func
    Func -->|Save| DDB
    Func -->|Publish| SNS

    style Func fill:#ffe066
    style S3 fill:#4ecdc4

Q: How do you design a multi-cloud strategy for a microservices platform?

Why interviewers ask this: Tests strategic thinking about vendor independence, latency, compliance, and the real cost of multi-cloud complexity.

Answer

Why multi-cloud?

Driver Explanation
Vendor lock-in avoidance No dependency on a single provider's SLAs or pricing changes
Regulatory compliance Data residency laws may require specific regions or providers
Resilience Cloud provider outages (rare but costly) — failover to another cloud
Best-of-breed services GCP BigQuery for analytics, AWS S3 for storage, Azure AD for identity
Cost optimization Leverage pricing differences across providers

Architecture strategies:

1. Active-Active Multi-Cloud:
   Traffic split across AWS + GCP simultaneously
   ✅ Maximum availability   ❌ Data synchronisation complexity
   Use: global platforms, financial services, > 99.99% SLA requirements

2. Active-Passive (DR) Multi-Cloud:
   Primary on AWS, failover to Azure
   ✅ Simpler data sync (one-way replication)   ❌ Failover has RTO cost
   Use: disaster recovery, most enterprise workloads

3. Services Split by Cloud:
   Auth on Azure AD, compute on AWS, ML on GCP
   ✅ Use each cloud's strengths   ❌ Cross-cloud latency and egress costs
   Use: data engineering, ML pipelines

Portability best practices:

# Use cloud-agnostic abstractions:

# 1. Kubernetes everywhere — same manifests, different cloud k8s managed service
#    AWS EKS  |  GCP GKE  |  Azure AKS

# 2. Terraform for IaC — provider-abstracted modules
provider "aws" { region = "us-east-1" }
# Swap provider block to replicate infra on Azure

# 3. Abstract cloud SDK calls behind interfaces
public interface ObjectStorage {
    void upload(String bucket, String key, byte[] data);
    byte[] download(String bucket, String key);
}
// AWS implementation: AmazonS3Client
// GCP implementation: Storage client
// Azure implementation: BlobServiceClient

# 4. Use portable message brokers (Confluent Kafka, Pulsar) over cloud-native (SQS, Pub/Sub)

Common Mistake

Multi-cloud sounds strategically sound but has high operational cost. Data egress fees between clouds are significant (often $0.08/GB). Latency between clouds adds 20–80ms. Start with multi-region within one cloud (much simpler), then consider multi-cloud only if you have a specific compelling driver — vendor lock-in risk, regulatory requirement, or a specific best-of-breed service need.