JavaBeans for Dummies

When working with Java one inevitably comes across the term “Java Bean”.

The term has the same problem that many software development terms have:

It is not clearly defined or gets used differently from its original definition.

“Java Beans” is a case of the latter.

TLDR

Nowadays a Java Bean is usually referred to as an object that follows these patterns:

In modern Java, JavaBeans are primarily used as simple data containers.

The getter/setter convention allows libraries to automatically convert data between Java objects, JSON payloads, and database tables.

For example Jackson uses JavaBeans getters/setters to deserialise Java objects from JSON.

The name

The term “Java Beans” originates from the Java Beans specification that was introduced in 1996 when Java 1.1 was released.

It’s called a “bean” because it’s meant to follow Java’s coffee-themed naming pattern.

JavaBeans™ History

The way we use Java Beans today has very little to do with their official specification.

JavaBeans™ were originally designed to be used in visual builders.

Here is the official definition from the 1996 JavaBeans API Specification:

A Java Bean is a reusable software component that can be manipulated visually in a builder tool.

Here is a historically accurate JavaBean™ that fully satisfies the specification:

import java.io.Serializable;
import java.beans.PropertyChangeSupport;
import java.beans.PropertyChangeListener;

public class CounterBean implements Serializable {
    // 1. Private properties
    private int count;
    
    // Initialized for event support, NOT marked transient so it survives serialization
    private PropertyChangeSupport changes;

    // 2. Public no-argument constructor
    public CounterBean() {
        this.count = 0;
        this.changes = new PropertyChangeSupport(this);
    }

    // 3. Standard Getter
    public int getCount() {
        return this.count;
    }

    // 3. Standard Setter with property change firing
    public void setCount(int newCount) {
        int oldCount = this.count;
        this.count = newCount;
        
        // Manual primitive wrapping (new Integer) required in JDK 1.1
        this.changes.firePropertyChange("count", 
                                        new Integer(oldCount), 
                                        new Integer(newCount));
    }

    // 4. Standard event listener registration for bound properties
    public void addPropertyChangeListener(PropertyChangeListener l) {
        this.changes.addPropertyChangeListener(l);
    }

    public void removePropertyChangeListener(PropertyChangeListener l) {
        this.changes.removePropertyChangeListener(l);
    }
}

JavaBeans allowed IBM and Borland to build Java IDEs that build on top of the JavaBeans specification, like Borland JBuilder and IBM VisualAge for Java.

In IBM VisualAge for Java you dropped beans onto a canvas in the Composition Editor and wired them together with connections, without writing the code yourself:

IBM VisualAge for Java Composition Editor connecting a button bean to an image bean

Selecting a bean opened a property sheet. Every entry in it comes from a getter/setter pair on the bean:

IBM VisualAge for Java property sheet for a selected bean

Borland JBuilder worked the same way. Its designer showed the components on the left and the bean’s properties and events on the right:

Borland JBuilder designer with an applet and its property sheet

Borland JBuilder 3 designer showing a Swing frame, component tree and properties

JavaBean™ vs Bean today

Today’s Java Beans are not typically used for Visual Builders anymore. They also rarely follow the official specification so they should not even be called “Java Beans”.

That’s why you often see them called simply “Beans” instead of “Java Beans”

A bean is more of a de facto specification:

They are used as data containers (also called data objects, domain models, or DTOs). They hold state using private fields and expose that state via public getters and setters.

The getter/setter convention allows libraries to automatically convert data between Java objects, JSON payloads, and database tables.

For example Jackson relies on JavaBeans to deserialise Java objects from JSON.

A typical example would be a Request DTO that gets used by a Framework like Spring.

Most real-world applications use Lombok to auto-generate the getters / setters to reduce the amount of code that has to be written for each data container.

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;

@Data                   // Generates getters, setters, toString, equals, and hashCode
@NoArgsConstructor      // Generates mandatory JavaBean no-arg constructor
@AllArgsConstructor     // Generates constructor with all fields
public class UserDto {
    private String username;
    private String email;
}

In this case Spring Boot’s @RequestBody annotation triggers the deserialisation when a HTTP request hits this endpoint. Spring Boot uses Jackson under the hood to deserialise the incoming JSON into a Java object.

package com.example.controller;

import com.example.dto.UserRegistrationDto;
import com.example.service.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping
    public ResponseEntity<String> registerUser(@RequestBody UserRegistrationDto userDto) {
        // The JavaBean carries user payload into the business service layer
        userService.createUser(userDto);
        return ResponseEntity.ok("User registered successfully: " + userDto.getUsername());
    }
}

Bean vs Java Record

When you look at the examples above you can see that Java requires you to write a lot of code for simple data containers. This led to third-party solutions like Lombok to become the de-facto standard in the Java community.

Java 16 introduced Records to eliminate boilerplate for data containers.

Here is a Bean compared to a Record:

Bean (19 Lines):

package com.example;

import java.io.Serializable;

public class UserBean implements Serializable {

    private String name;
    private int age;

    public UserBean() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Record (1 Line):

package com.example;

import java.io.Serializable;

public record UserRecord(String name, int age) implements Serializable {}

Records have a very nice short syntax that makes you wonder:

Why not always use Records instead of Beans?

The simple truth is that you cannot always use Records instead of Java Beans.

Records don’t only have a different syntax, they are also different from Java Beans in other ways.

The main difference is that Records are immutable at the language level.

This design choice makes some use cases of Java Beans impossible.

As explained above, a lot of frameworks and libraries rely on the de-facto Beans standard.

The table below outlines key use cases, which option to choose, and why.

Use CaseBest ChoiceReason
Database Entities (JPA/Hibernate)JavaBeanRequires no-arg constructor, mutability, and proxy inheritance
API Requests / Responses (JSON DTOs)RecordImmutable data transfer; safe, concise, and clean
UI Form Models / Stateful ObjectsJavaBeanIn-place property mutation as fields are edited
Pattern Matching / Value ObjectsRecordBuilt-in value equality (equals/hashCode) and deconstruction
Inheritance Hierarchies (extends Base)JavaBeanRecords cannot extend or be extended by other classes