Sitemap

Java in the Cloud

The Enterprise Development Language

Java is one of the very first languages I have ever learned, I just had to sneak it in here, also because it is robust and… my favourite. So, it’s a save the best for “lastish” kind of situation.

Press enter or click to view image in full size

So, you’ve built and orchestrated a cloud-native portfolio with Python, SQL, Docker, and Kubernetes.

But cloud computing is not only about startups and small services. Banks, telecommunications companies, retailers, and governments run systems that must process millions of requests reliably for years.

Think about it:

  • Mission-critical transactions? → They need reliability and strong typing
  • Large engineering teams? → They need maintainable, well-structured code
  • Distributed cloud services? → They need mature frameworks and observability
  • Enterprise integrations? → They need a proven ecosystem

This is where Java comes in.

Java remains the backbone of enterprise cloud applications. From Spring Boot microservices to Apache Kafka streaming platforms, Java powers mission-critical cloud infrastructure worldwide.

In this post, Java joins our portfolio. We won’t throw away the Python backend. We’ll add Java next to it, the way real companies do: two small Java programs that talk to the Flask API over HTTP, run in Docker, and deploy to the same Kubernetes cluster from the previous post.

Topics

  1. How Java runs: JDK, JVM, and bytecode
  2. Setting up Java on Windows
  3. Java fundamentals: A Portfolio Health Checker
  4. Compiling, Inspecting bytecode, And Packaging a JAR
  5. Containerising Java with Docker
  6. Running Java as a Kubernetes Job
  7. Spring Boot: A Java Microservice For The Portfolio
  8. Deploying the Spring Boot Service To Kubernetes
  9. Java’s Cloud Ecosystem And Best Practices
  10. Troubleshooting

1 | How Java Runs

Python runs your .py file directly through the Python interpreter. Java adds a compile step:

PortfolioHealthCheck.java ──javac──► PortfolioHealthCheck.class ──java──► JVM
source code you write bytecode: the same file runs it on Windows,
for every operating system Linux, macOS, x86, ARM
  • Source code (.java files) is what you write.
  • javac, the Java compiler, checks your code and turns it into bytecode (.class files). Bytecode isn't machine code for a real CPU. It's a set of instructions for the JVM.
  • The JVM (Java Virtual Machine) loads the bytecode and runs it. While your program runs, the JVM’s JIT (just-in-time) compiler turns frequently used code into fast native machine code, and its garbage collector frees memory you no longer use.

That’s Java’s famous promise: write once, run anywhere. You compile on your Windows laptop, and the same bytecode runs in a Linux container in the cloud.

JDK vs. JRE vs. JVM

The JVM (Java Virtual Machine) is the engine that runs Java bytecode and is always present as part of the JRE and JDK.

The JRE (Java Runtime Environment) includes the JVM and the standard library, making it suitable for running Java applications, for example, in a production container.

The JDK (Java Development Kit) includes the runtime environment and development tools such as javac, jar, javap, and jshell, which you need to write, compile, build, and debug Java applications on your laptop.

Why This Matters in the Cloud

  • Portability: one build artifact (a JAR file) runs on any cloud, operating system, or CPU architecture that has a JVM.
  • Long-running performance: the JIT optimizes code while a service runs, so busy APIs get faster after they warm up.
  • The trade-off: JVM apps start slower and use more memory than a small Python script. Where startup time matters, such as serverless functions, tools like GraalVM native images and AWS Lambda SnapStart help.

Java Versions and LTS

A new Java version ships every six months.

Every two years, one of them is a Long-Term Support (LTS) release that receives security updates for years. Java 17, 21, and 25 are LTS releases, and cloud teams standardize on LTS versions. This post uses Java 21, which every tool and base image in this post supports.

Java for Python Developers

You learned Python already, so here’s a map from one to the other:

+-------------------+----------------------------+------------------------------------------+

| Feature | Python | Java |
+-------------------+----------------------------+------------------------------------------+

| Typing | Dynamic: level = 75 | Static: int level = 75; (or var level) |
| Code blocks | Indentation | Curly braces { } and semicolons |
| Running | python app.py | javac then java, or java -jar app.jar |
| Dependencies | pip + requirements.txt | Maven + pom.xml (or Gradle) |
| Web framework | Flask | Spring Boot |
| A route | @app.route('/api/health') | @GetMapping("/api/health") |
| "No value" | None | null |
| Handling errors | try / except | try / catch |
+-------------------+----------------------------+------------------------------------------+

2 | Set Up Java on Windows

Install Eclipse Temurin JDK 21, a free, open-source build of OpenJDK:

winget install EclipseAdoptium.Temurin.21.JDK

If you prefer a regular installer, download the JDK 21 .msi from adoptium.net and enable Set JAVA_HOME variable during setup.

Close and reopen your terminal so it picks up the updated PATH, then check both tools:

java --version
javac --version

Both should report version 21.

You don’t need to install Maven.

The Spring Boot project we generate in Section 7 includes the Maven Wrapper (mvnw.cmd), which downloads the right Maven version for you. You'll also use Docker Desktop and kubectl from Post 10.

🏗️ Let’s Build

3 | A Portfolio Health Checker

Our first Java program checks the Flask API.

It calls four endpoints, prints the result of each one, and exits with code 0 if everything is up, or 1 if anything failed.

Exit codes are how scripts, CI pipelines, and Kubernetes Jobs know whether a program succeeded, so this small program is a real cloud tool.

Create LanguagesPortfolio/java/health-check/PortfolioHealthCheck.java:

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;

/**
* Checks the portfolio's Flask API and reports which endpoints are up.
* Exit code 0 = all checks passed, 1 = at least one check failed.
*/
public class PortfolioHealthCheck {

// A record is a compact, immutable data class. Java generates the
// constructor, accessor methods, equals(), hashCode() and toString().
record CheckResult(String path, boolean up, String detail, long millis) {}

// static final = one shared constant. List.of() creates an unmodifiable list.
static final List<String> ENDPOINTS = List.of(
"/api/health",
"/api/skills",
"/api/projects",
"/api/analytics/progress"
);

public static void main(String[] args) {
// Read configuration from the environment, with a local default.
String baseUrl = System.getenv().getOrDefault("PORTFOLIO_API_URL", "http://localhost:5000");

HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.build();

System.out.println("Checking portfolio API at " + baseUrl);

int failures = 0;
for (String path : ENDPOINTS) {
CheckResult result = check(client, baseUrl, path);
System.out.printf(" %-4s %-25s %-20s %5d ms%n",
result.up() ? "UP" : "DOWN", result.path(), result.detail(), result.millis());
if (!result.up()) {
failures++;
}
}

System.out.println(failures == 0 ? "All checks passed." : failures + " check(s) failed.");
System.exit(failures == 0 ? 0 : 1);
}

static CheckResult check(HttpClient client, String baseUrl, String path) {
HttpRequest request = HttpRequest.newBuilder(URI.create(baseUrl + path))
.timeout(Duration.ofSeconds(5))
.GET()
.build();

long start = System.nanoTime();
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
long millis = (System.nanoTime() - start) / 1_000_000;
boolean up = response.statusCode() == 200;
return new CheckResult(path, up, "HTTP " + response.statusCode(), millis);
} catch (IOException | InterruptedException e) {
// Connection refused, timeout, unknown host: the endpoint is down.
long millis = (System.nanoTime() - start) / 1_000_000;
return new CheckResult(path, false, e.getClass().getSimpleName(), millis);
}
}
}

What This Code Means

  • Classes: All Java code lives in classes, and a public class (like public class PortfolioHealthCheck) must match its file name.
  • Main Method: The public static void main(String[] args) method serves as the official entry point that the JVM calls to start the program.
  • Static Typing: Explicit declarations like String baseUrl and int failures allow the compiler to catch type mistakes before the code ever runs.
  • Generics: Container types like List<String> enforce strict rules so you cannot accidentally insert a number into a list of strings.
  • Records: The CheckResult record handles immutable data without boilerplate, letting you read values cleanly via result.up().
  • Checked Exceptions: The language forces you to safely handle unpredictable errors by catching IOException and InterruptedException.
  • Timeouts: Network configurations like connectTimeout() prevent a single hung server from freezing the entire program indefinitely.
  • Environment Config: Using System.getenv(...) ensures the exact same code runs seamlessly across your laptop, Docker, and Kubernetes.
  • Exit Codes: Triggering System.exit(...) tells the calling pipeline or terminal whether the health check passed or failed.

Run It

Start the Flask backend:

# Terminal 1, in LanguagesPortfolio\backend
.\venv\Scripts\Activate.ps1
python app.py

Then run the Java program:

# Terminal 2, in LanguagesPortfolio\java\health-check
java PortfolioHealthCheck.java
$LASTEXITCODE

java PortfolioHealthCheck.java compiles the file in memory and runs it straight away. Java 11 added this shortcut for single-file programs.

You should see something like:

Checking portfolio API at http://localhost:5000
UP /api/health HTTP 200 12 ms
UP /api/skills HTTP 200 8 ms
UP /api/projects HTTP 200 6 ms
UP /api/analytics/progress HTTP 200 9 ms
All checks passed.
0

Now stop Flask with Ctrl+C and run the program again.

Every line shows DOWN with an error such as ConnectException, the last line says 4 check(s) failed., and $LASTEXITCODE prints 1.

The program is doing its job.

4 | Compile, Inspect, and Package

The shortcut hides the compile step. Do it by hand to see what the JVM actually runs:

# In LanguagesPortfolio\java\health-check

# Compile: .java source -> .class bytecode, written to the "classes" folder
javac -d classes PortfolioHealthCheck.java

# List the output: one .class file per class
Get-ChildItem classes

# Run the bytecode (-cp tells the JVM where to find classes)
java -cp classes PortfolioHealthCheck

# Look inside the bytecode
javap -c -cp classes PortfolioHealthCheck

classes now contains PortfolioHealthCheck.class and PortfolioHealthCheck$CheckResult.class, because the record is a class of its own. In the javap output you'll see JVM instructions such as getstatic, invokestatic, and invokevirtual. You never write these by hand, but this is the portable format that every JVM runs.

Now package the classes into a JAR:

jar --create --file health-check.jar --main-class PortfolioHealthCheck -C classes .
java -jar health-check.jar

A JAR (Java ARchive) is a ZIP file of .class files plus a META-INF/MANIFEST.MF file. --main-class records the entry point in the manifest, so java -jar knows which main method to run. The JAR is Java's unit of deployment: you build it once and ship the same file everywhere.

Build output doesn’t belong in Git.

Add these lines to LanguagesPortfolio/.gitignore:

java/health-check/classes/
java/health-check/*.jar
java/insights/target/

5 | Containerise It

Create LanguagesPortfolio/java/health-check/Dockerfile:

# Stage 1: build with the full JDK (compiler and tools)
FROM eclipse-temurin:21-jdk AS build
WORKDIR /src
COPY PortfolioHealthCheck.java .
RUN javac -d classes PortfolioHealthCheck.java \
&& jar --create --file health-check.jar --main-class PortfolioHealthCheck -C classes .

# Stage 2: run with only the JRE (smaller image, no compiler)
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /src/health-check.jar .
# Run as a regular user instead of root
USER 1000
ENTRYPOINT ["java", "-jar", "health-check.jar"]

This is a multi-stage build. The first stage uses the JDK to compile the code. The second stage starts fresh from the smaller JRE image and copies in only the finished JAR. The compiler and the source code never reach the final image, so it’s smaller and gives an attacker fewer tools to work with.

Why Eclipse Temurin? Older tutorials use the openjdk Docker image, but that image is deprecated. eclipse-temurin is a Docker Official Image of the Adoptium project's OpenJDK builds, with both -jdk and -jre tags.

Build and run it while Flask is running on your machine:

# In LanguagesPortfolio\java\health-check
docker build -t portfolio-health-check:1.0 .
docker run --rm -e PORTFOLIO_API_URL=http://host.docker.internal:5000 portfolio-health-check:1.0

Inside a container, localhost means the container itself, not your laptop. Docker Desktop provides the hostname host.docker.internal for reaching your machine. In Kubernetes we'll use a Service name instead.

The JVM Knows It’s in a Container

Modern JVMs read the container’s memory limit. By default, the JVM uses at most 25% of the container’s memory for its heap, the area where your objects live. That’s cautious for a container that runs only one Java app, so cloud teams commonly raise it:

-XX:MaxRAMPercentage=75.0

We’ll pass this option through the JAVA_TOOL_OPTIONS environment variable in Kubernetes. The JVM reads that variable at startup and prints a Picked up JAVA_TOOL_OPTIONS line, so you can confirm it took effect.

6 | Run It as a Kubernetes Job

In Post 10 we used Deployments, which keep a program running forever. A Job runs a program until it finishes, and the program’s exit code decides whether the Job succeeded. That fits our health checker exactly.

Create LanguagesPortfolio/k8s/java-health-check.yaml:

apiVersion: batch/v1
kind: Job
metadata:
name: java-health-check
namespace: languages-portfolio
spec:
backoffLimit: 0 # Don't retry: a failed check should stay visible
ttlSecondsAfterFinished: 3600 # Delete the finished Job after one hour
template:
spec:
restartPolicy: Never # Jobs must use Never or OnFailure
containers:
- name: health-check
image: portfolio-health-check:1.0
imagePullPolicy: IfNotPresent # Use the local image instead of pulling from Docker Hub
env:
- name: PORTFOLIO_API_URL
value: http://backend-service:5000 # Kubernetes DNS name of the Flask Service
resources:
limits:
memory: "256Mi"

backend-service is the Service defined in k8s/backend.yaml. Inside the languages-portfolio namespace, Kubernetes DNS resolves that name to the Service's IP address, and the Service forwards each request to one of the Flask pods. The Java program finds the Python API without knowing any pod's IP address.

Deploy it and check the result:

# In LanguagesPortfolio

# Minikube only: copy your local image into the cluster
minikube image load portfolio-health-check:1.0

kubectl apply -f k8s\java-health-check.yaml
kubectl get jobs -n languages-portfolio
kubectl logs job/java-health-check -n languages-portfolio

The Job takes a few seconds to finish. COMPLETIONS 1/1 means the program exited with code 0. If it stays at 0/1, at least one check failed. The logs show the same table you saw on your laptop, so you can see which endpoint was down.

You can’t change a Job after creating it, so delete it before running it again:

kubectl delete job java-health-check -n languages-portfolio
kubectl apply -f k8s\java-health-check.yaml

To run the check on a schedule, put the same pod template inside a CronJob with schedule: "*/15 * * * *" (every 15 minutes).

7 | From Script to Service: Spring Boot

The health checker is a script: it runs and then exits. Enterprise systems are mostly services, programs that run all the time and answer requests. In Java, the standard way to build one is Spring Boot.

Spring Boot gives you:

  • An embedded web server. Tomcat is packaged inside your JAR, so java -jar starts a complete web service.
  • Dependency injection. A class lists what it needs in its constructor, and Spring creates and connects the objects for you.
  • Auto-configuration. Add a dependency, and Spring Boot configures it with sensible defaults.
  • Actuator. Built-in health and metrics endpoints that Kubernetes can probe.

We’ll build java-insights, a small service that reports its own status and whether the Flask backend is reachable.

Step 1: Generate the Project

Go to start.spring.io and:

Get Ntombizakhona Mabaso’s stories in your inbox

Join Medium for free to get updates from this writer.

Project is set to Maven.
Language is chosen as Java.
Spring Boot version uses the default stable release.
Group identifier is configured as com.portfolio.
Artifact name is specified as insights.
Packaging type is defined as Jar.
Java version is selected as 21.
Dependencies include Spring Web and Spring Boot Actuator.

Click Generate and unzip the download into LanguagesPortfolio\java.

The zip contains an insights folder, so the project ends up inLanguagesPortfolio\java\insights.

Open src/main/java/com/portfolio/insights/InsightsApplication.java. Spring Initializr generated it for you:

@SpringBootApplication
public class InsightsApplication {
public static void main(String[] args) {
SpringApplication.run(InsightsApplication.class, args);
}
}

It’s an ordinary main method, like the one in our health checker. SpringApplication.run starts Spring, which finds your classes in this package, configures everything, and starts the web server. @SpringBootApplication switches all of that on.

Step 2: Write a REST Controller

Create src/main/java/com/portfolio/insights/StatusController.java:

package com.portfolio.insights;

import java.time.Instant;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;

@RestController
@RequestMapping("/api/java")
public class StatusController {

// The JSON shape this endpoint returns. Spring converts records to JSON automatically.
public record Status(String service, String javaVersion, String flaskBackend, String checkedAt) {}

private final RestClient flaskApi;

// Spring calls this constructor and passes in the configured Flask URL.
public StatusController(@Value("${portfolio.api.url}") String portfolioApiUrl) {
this.flaskApi = RestClient.create(portfolioApiUrl);
}

@GetMapping("/status")
public Status status() {
String flaskBackend;
try {
// Java calling Python: GET <flask-url>/api/health
flaskApi.get().uri("/api/health").retrieve().toBodilessEntity();
flaskBackend = "UP";
} catch (RestClientException e) {
// Flask is down, unreachable, or returned an error status
flaskBackend = "DOWN";
}
return new Status(
"java-insights",
System.getProperty("java.version"),
flaskBackend,
Instant.now().toString());
}
}

What The Annotation Means

  • The @RestController annotation marks a class as a controller where every method automatically returns JSON data in response to HTTP requests, which functions similarly to Flask functions using @app.route that return a jsonify(...) response.
  • The @RequestMapping("/api/java") annotation defines a base URL prefix for all endpoints within that class, operating much like a Flask Blueprint with a designated url_prefix.
  • The @GetMapping("/status") annotation specifies that a method will handle incoming HTTP GET requests for the combined path (e.g., /api/java/status), which serves the same purpose as Flask’s @app.route('/api/java/status', methods=['GET']).
  • The @Value("${portfolio.api.url}") annotation injects a configuration value directly into a variable from the application properties, matching the behavior of loading environment variables in Flask via os.getenv(...) within a config.py file.

This small class carries two cloud lessons:

  • Service-to-service calls. RestClient is Spring's HTTP client. Java and Python don't share any code. They share an HTTP contract, so either side can be rewritten without the other noticing.
  • Failure isolation. If Flask is down, this service still answers and reports DOWN instead of crashing. One broken service shouldn't take others down with it.

RestClient requires Spring Boot 3.2 or newer, and Initializr's default version is newer than that.

Step 3: Configure It

Replace the contents of src/main/resources/application.properties with:

spring.application.name=insights
server.port=8080

# Where the Flask API lives. Uses the PORTFOLIO_API_URL environment
# variable if it's set, otherwise falls back to localhost.
portfolio.api.url=${PORTFOLIO_API_URL:http://localhost:5000}

# Expose /actuator/health/liveness and /actuator/health/readiness
# for Kubernetes probes (and when running locally).
management.endpoint.health.probes.enabled=true

In a .properties file, comments must be on their own line. Anything after the = is part of the value.

This follows the same rule as the Flask backend’s config.py: the code reads its settings from the environment, so one JAR works on your laptop, in Docker, and in Kubernetes.

Step 4: Run and Test

With Flask still running, start the Java service:

# In LanguagesPortfolio\java\insights
.\mvnw.cmd spring-boot:run

The first run downloads Maven and the project’s libraries, so it takes a few minutes. When you see Started InsightsApplication, test it from another terminal. curl.exe comes with Windows and prints the raw JSON:

curl.exe http://localhost:8080/api/java/status
curl.exe http://localhost:8080/actuator/health

The first command prints something like the line below. Spring turned the Status record into JSON, with one field per record component:

{"service":"java-insights","javaVersion":"21.0.x","flaskBackend":"UP","checkedAt":"2026-10-03T09:30:12.345678Z"}

The second returns Actuator’s health report, which includes "status":"UP". Now stop Flask and call /api/java/status again. flaskBackend changes to DOWN, but the Java service keeps answering.

8 | Deploy the Spring Boot Service to Kubernetes

Package the JAR

Stop the running service with Ctrl+C, then build and run the JAR:

# In LanguagesPortfolio\java\insights
.\mvnw.cmd clean package
java -jar target\insights-0.0.1-SNAPSHOT.jar

package compiles the code, runs the test Initializr generated, and builds target\insights-0.0.1-SNAPSHOT.jar. This is a Spring Boot "fat JAR": it contains your classes, every library, and the embedded Tomcat server. That single file is the whole application.

Stop it with Ctrl+C once you've seen it start.

Containerise

Create LanguagesPortfolio/java/insights/Dockerfile:

FROM eclipse-temurin:21-jre
WORKDIR /app
# The JAR built by "mvnw.cmd clean package"
COPY target/*.jar app.jar
USER 1000
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

This time Maven builds the JAR on your machine, and the Dockerfile only packages it, so one stage is enough. The multi-stage pattern from Section 5 works here too, and CI pipelines usually use it.

# In LanguagesPortfolio\java\insights
docker build -t portfolio-java-insights:1.0 .

Deploy

Create LanguagesPortfolio/k8s/java-insights.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
name: java-insights
namespace: languages-portfolio
labels:
app: java-insights
spec:
replicas: 1
selector:
matchLabels:
app: java-insights
template:
metadata:
labels:
app: java-insights
spec:
containers:
- name: java-insights
image: portfolio-java-insights:1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: PORTFOLIO_API_URL
value: http://backend-service:5000
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=75.0"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 20
---
apiVersion: v1
kind: Service
metadata:
name: java-insights-service
namespace: languages-portfolio
spec:
selector:
app: java-insights
ports:
- port: 8080
targetPort: 8080
type: ClusterIP

What’s different from the Flask Deployment:

  • PORTFOLIO_API_URL points at the Flask Service, exactly like the Job does.
  • The probes use Actuator. The Flask backend’s probes in k8s/backend.yaml call /api/health. Here, readiness tells Kubernetes when to send traffic to the pod, and liveness tells it when to restart the container.
  • The initial delays are longer because the JVM and Spring need several seconds to start.
  • JAVA_TOOL_OPTIONS lets the heap use 75% of the 512Mi memory limit.

Deploy and test:

# In LanguagesPortfolio

# Minikube only
minikube image load portfolio-java-insights:1.0

kubectl apply -f k8s\java-insights.yaml
kubectl get pods -n languages-portfolio -l app=java-insights
kubectl port-forward service/java-insights-service 8080:8080 -n languages-portfolio

Then, in another terminal:

curl.exe http://localhost:8080/api/java/status

"flaskBackend":"UP" proves that Java reached Python through Kubernetes DNS inside the cluster. Your portfolio's backend now runs two languages.

⛔ End of Building Tutorial⛔

9 | Java’s Cloud Ecosystem

These two programs use a small slice of what Java offers. Here’s where to go next:

Web APIs, Microservices, and Databases

  • Web APIs and microservices are commonly built using robust frameworks like Spring Boot, Quarkus, and Micronaut.
  • Database interactions are handled through core technologies like JDBC or object-relational mapping frameworks like Spring Data JPA and Hibernate, which can be used against systems like your MySQL database.
  • Database migrations and version control for schemas are managed using tools like Flyway or Liquibase.

Messaging, Security, and Observability

  • Messaging and streaming architectures rely on clients for Apache Kafka (which itself is written in Java and Scala) or RabbitMQ.
  • Application security, including authentication and authorization via OAuth2 and JWT, is typically implemented using Spring Security.
  • Observability, metrics, and distributed tracing are configured using Spring Boot Actuator, Micrometer, and OpenTelemetry.

Cloud, Operations, and Building

  • Cloud SDKs allow seamless integration with major platforms through the AWS SDK for Java, Azure SDK for Java, and Google Cloud client libraries.
  • Serverless deployment targets platforms like AWS Lambda Java runtimes utilizing SnapStart, as well as Azure Functions.
  • Fast startup times and reduced memory footprints are achieved by compiling to a GraalVM native image or using OpenJDK CRaC (Coordinated Restore at Checkpoint).
  • Project builds and dependency management are handled by the industry-standard build tools Maven or Gradle.

Best Practices for Java in the Cloud

  • Use an LTS JDK and pin your image tags (eclipse-temurin:21-jre, not latest) so builds are repeatable.
  • Ship the JRE, not the JDK, in production images, and run as a non-root user.
  • Read configuration from the environment. Keep passwords in Kubernetes Secrets as in Post 10, never in code or in application.properties.
  • Set timeouts on every outbound call. The health checker does. A production version of StatusController should too.
  • Expose health endpoints and connect them to probes. Actuator does most of the work.
  • Size memory on purpose. Set a container memory limit and tune MaxRAMPercentage to match it.
  • Log to standard output. Kubernetes collects it for kubectl logs, so don't write log files inside the container.

10 | Troubleshooting

Local Environment and Compilation Issues

  • If java is not recognized, the likely cause is that the terminal was opened before Java was installed. You can fix this by opening a new terminal and checking the installation with Get-Command java.
  • When mvnw.cmd complains about JAVA_HOME, the installer did not set the environment variable. To resolve this, run $env:JAVA_HOME = Split-Path (Split-Path (Get-Command java).Source) in that terminal, or set it permanently under Environment Variables in your Windows settings.
  • If you get an error stating that class PortfolioHealthCheck is public, should be declared in a file named PortfolioHealthCheck.java, the file name and the class name do not match. You must rename the file to match the class name exactly, including identical capital letters.

Connection and Network Issues

  • When every check shows DOWN with a ConnectException, Flask either is not running or the configured URL is incorrect. To fix this, start Flask and verify your environment variable by checking $env:PORTFOLIO_API_URL.
  • If the container cannot reach localhost:5000, it is because localhost refers to the container itself. You should change the connection URL to use http://host.docker.internal:5000 instead.
  • If you find that port 8080 is already in use, another program or a second copy of the service is occupying that port. You need to stop the conflicting program, or start your service on a different port by running $env:SERVER_PORT = "8081".

Kubernetes Deployment Issues

  • When encountering ErrImagePull or ImagePullBackOff errors, your cluster cannot see your local container image. If you are using Minikube, fix this by running minikube image load <image>. Otherwise, push the image to a registry and update the image: field in your deployment configuration.
  • If a Pod status shows OOMKilled, the memory limit configured in Kubernetes is too low for the Java Virtual Machine (JVM). You can resolve this by raising limits.memory in your pod specification or lowering the MaxRAMPercentage in your Java options.
  • When re-applying a Job fails with an error stating a field is immutable, it means Kubernetes Jobs cannot be modified after they are created. To fix this, delete the existing Job and apply your configuration file again.

Final Thoughts

Java adds a proven enterprise layer to your cloud toolkit. You compiled source code into bytecode, packaged it as a JAR, shipped it in a container, and ran it on the same Kubernetes platform you learned in the previous article.

Then you built a Spring Boot microservice that talks to your Python API over HTTP.

The syntax is only part of the lesson.

Java’s real value in the cloud comes from its mature ecosystem, predictable structure, and ability to support large systems and engineering teams over time. As your portfolio now shows, it doesn’t have to replace anything: Java and Python run side by side, connected by HTTP and Kubernetes DNS.

Concluding Remarks

Java completes the application language layer of our cloud journey. Next, we move into Infrastructure as Code with Terraform: codifying the cloud itself.

Press enter or click to view image in full size
Java — The Enterprise Development Language

Languages Portfolio Progress

✅ 01 — English: Technical Communication And Prompt Engineering

✅ 02 — Mathematics: Logic And Problem-Solving Fundamentals

✅ 03 — HTML: Web Structure And Semantic Markup

✅ 04 — CSS: Styling And Responsive Design

✅ 05 — JavaScript: Interactive Web Development

✅ 06 — Python: Backend Logic And Cloud Automation

✅ 07 — Git: Version Control And Deployment

✅ 08 — Linux: The Cloud Operating System

✅ 09 — SQL: Cloud Data Management

✅ 10 — Kubernetes: Container Orchestration At Scale

✅ 11 — Java: Enterprise Cloud Development

⬜ 12 — Terraform: Infrastructure as Code

⬜ Bonus — Agentic IDEs: AI-Powered Cloud Development

Additional Resources

The Java™ Tutorials

The Java Tutorials are practical guides for programmers who want to use the Java programming language to create applications.

They include hundreds of complete, working examples, and dozens of lessons. Groups of related lessons are organized into “trails”.

Java

Java is a compiled, object-oriented, highly portable programming language.

Java is statically typed and features a similar syntax to C. It comes with a large library of readily usable functions, the Java Software Development Kit (SDK).

Written by Ntombizakhona Mabaso

Cloud Computing & Artificial Intelligence