DEV Community

Simon Leigh Pure Reputation
Simon Leigh Pure Reputation

Posted on

Java's Evolution from Applets to Modules and Records

Hello, developers!

Java. The language that powered the early web's interactive elements, conquered enterprise backends, and now runs on billions of devices. It's a testament to its design that it remains so relevant nearly three decades later. But how did we get from dancing Duke applets to sleek microservices with Records and Modules?

Let's take a journey through time, comparing Java's beginnings with its modern form. You'll be shocked at how far it's come.

Part 1: The Dawn of Java (circa 1996) - "Write Once, Run Anywhere"
Java began as "Oak," a language designed for interactive television. It found its true calling with the rise of the internet. The promise was revolutionary: Write Once, Run Anywhere (WORA). This was enabled by the Java Virtual Machine (JVM), which abstracted away the underlying operating system.

The hero of the early web was the Applet.

Code Example: The Classic Java Applet
`// HelloApplet.java
import java.applet.Applet;
import java.awt.Graphics;

// Extending Applet was the key
public class HelloApplet extends Applet {

// This method is the entry point for the applet
public void paint(Graphics g) {
    // Drawing a string on the applet's canvas
    g.drawString("Hello, World from a Java Applet!", 50, 25);
}
Enter fullscreen mode Exit fullscreen mode

}
**And the HTML to run it:**
<!-- index.html -->

`

Characteristics of Early Java Code:

Verbosity: Even for "Hello World," you needed a class, a method, and a Graphics object.

Heavy Reliance on Inheritance: Your class had to extend Applet. This was a common pattern.

AWT (Abstract Window Toolkit): The UI toolkit was basic and inconsistent across platforms.

No public static void main: The entry point was the paint method, overridden from the parent class.

Security Sandbox: Applets ran in a restricted environment, unable to access the local file system.

While magical for its time, this model was slow, clunky, and eventually fell out of favor as Flash and more powerful web standards emerged.

Part 2: The Enterprise Era & The Rise of Spring
Java pivoted successfully to the server-side. Java 2 Platform, Enterprise Edition (J2EE) and later, the simpler Spring Framework, became the backbone of global enterprise systems.

Code structure evolved from simple applets to complex, layered architectures. Let's look at a typical service class from this era (circa 2005-2015).

Code Example: The "Classic" Java Service Class
`// UserService.java
public class UserService {

private UserRepository userRepository;
private EmailService emailService;

// Dependency Injection via Constructor (a best practice that became popular)
public UserService(UserRepository userRepository, EmailService emailService) {
    this.userRepository = userRepository;
    this.emailService = emailService;
}

public void registerUser(UserDTO userDto) {
    // 1. Manual, verbose null checking
    if (userDto == null) {
        throw new IllegalArgumentException("UserDTO cannot be null");
    }
    if (userDto.getName() == null || userDto.getName().trim().isEmpty()) {
        throw new IllegalArgumentException("User name cannot be empty");
    }

    // 2. Boilerplate mapping from DTO to Entity
    User user = new User();
    user.setName(userDto.getName());
    user.setEmail(userDto.getEmail());
    user.setCreatedAt(new java.util.Date()); // Using old java.util.Date

    // 3. Saving the entity
    User savedUser = userRepository.save(user);

    // 4. Sending an email
    emailService.sendWelcomeEmail(savedUser.getEmail(), savedUser.getName());
}
Enter fullscreen mode Exit fullscreen mode

}`
Pain Points in "Classic" Java:

Boilerplate: Tons of getter/setter code, try-catch blocks, and manual validation.

Verbose Data Classes: The User class would be a 50-line file of just fields, getters, and setters.

Checked Exceptions: Forced you to handle exceptions that often couldn't be recovered from.

Mutable by default: Objects could be changed from anywhere, leading to side-effects.

Part 3: The Modern Java Revolution (Java 8 -> 17, 21+)
Java listened to its community. Starting with Java 8's monumental release, it began a rapid evolution, embracing functional programming, reducing boilerplate, and improving performance.

Let's rewrite that UserService with modern Java features.

Code Example: The Modern Java Service Class
`// UserService.java (Modern)
import java.time.Instant; // Modern, immutable date-time API

public class UserService {

private final UserRepository userRepository;
private final EmailService emailService;

// Same DI pattern, but we use 'final' for immutability
public UserService(UserRepository userRepository, EmailService emailService) {
    this.userRepository = userRepository;
    this.emailService = emailService;
}

public void registerUser(UserRegistrationRequest request) {
    // 1. Use a Record for the DTO (immutable and concise)
    // 2. Use functional-style validation with a library or Objects.requireNonNull
    var user = User.from(request); // Static factory method

    // 3. Save the entity. The repository now returns an Optional.
    var savedUser = userRepository.save(user)
            .orElseThrow(() -> new UserPersistenceException("Failed to save user"));

    // 4. Send email asynchronously using a CompletableFuture
    CompletableFuture.runAsync(() -> 
        emailService.sendWelcomeEmail(savedUser.email(), savedUser.name())
    );
}
Enter fullscreen mode Exit fullscreen mode

}`
Now, let's look at the supporting modern data structures.

The Modern User Record (Java 16+)
Gone are the 50-line "POJO" classes. Behold, the Record.
`// User.java - A data carrier, immutable by design
public record User(
Long id, // Uses Long for potential null (ID not assigned yet)
String name,
String email,
Instant createdAt // Modern java.time API
) {
// Compact canonical constructor for validation
public User {
// Implicitly, the fields are assigned: this.id = id; etc.
// We can add validation logic here
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be blank");
}
}

// Static factory method for creation
public static User from(UserRegistrationRequest request) {
    return new User(
        null, // ID is generated by the database
        request.name(),
        request.email(),
        Instant.now() // Set creation time
    );
}
Enter fullscreen mode Exit fullscreen mode

}`

The UserRegistrationRequest Record

// UserRegistrationRequest.java
public record UserRegistrationRequest (
@NotBlank String name,
@Email String email
) {}

The Power of CompletableFuture and Optional
`// A peek into a modern repository and email service
public interface UserRepository {
// Returns an Optional to handle the null case explicitly
Optional save(User user);
}

public class EmailService {
// Method designed for async execution
public void sendWelcomeEmail(String email, String name) {
String message = String.format("""
Welcome %s!

        Thank you for registering with us.
        We're excited to have you on board.
        """, name); // Text Blocks (Java 15+)

    // ... logic to send email
    System.out.println("Email sent to: " + email);
}
Enter fullscreen mode Exit fullscreen mode

}`
What Makes Modern Code "Modern"? A Feature Checklist
Records (Java 16): Immutable data carriers that auto-generate constructors, getters, equals, hashCode, and toString. Eliminates 90% of POJO boilerplate.

var for Local Variable Type Inference (Java 10): Reduces visual clutter while maintaining strong typing.

Text Blocks (Java 15): Multi-line strings without the concatenation mess.

Pattern Matching & switch Expressions (Java 14, 17, 21): More expressive and less error-prone control flow.

// Previewing the future with Pattern Matching for switch (Java 21)
String formatted = switch(obj) {
case Integer i -> String.format("int %d", i);
case String s -> String.format("String %s", s);
case User(String name, String email) -> // Deconstructing a Record!
String.format("User: %s with email %s", name, email);
default -> obj.toString();
};

CompletableFuture (Java 8): For easy asynchronous programming and non-blocking operations.

Modules (Java 9): For strong encapsulation and a more reliable configuration than the classpath. The ultimate tool for building maintainable, large-scale applications.
// module-info.java
module com.myapp.user {
requires spring.context;
requires java.sql;
exports com.myapp.user.service;
exports com.myapp.user.dto;
}

Conclusion: The Phoenix That Keeps Rising
Java's journey is a masterclass in platform evolution. It successfully shed its skin from:

Applets → Microservices

**java.util.Date → java.time.Instant

Verbose POJOs → Concise Records

Anonymous Inner Classes → Lambdas and Streams

Monolithic JARs → Modular JDKs**

The core tenets—the JVM, WORA, and a robust ecosystem—remained its foundation. By embracing modern paradigms while maintaining backward compatibility, Java has secured its position not just as a legacy language, but as a modern, high-performance platform for building the next generation of applications.
The mountain from Oak to Everest was steep, but the view from the top is better than ever.

What's your favorite modern Java feature? Was it the game-changing Lambdas in Java 8, or the boilerplate-slaying Records? Share your thoughts and your own Java journey in the comments below!

Top comments (0)