Modern Java

Records and Modern Data Models

ReadingPreview

আপনি একটি free preview lesson দেখছেন।

Lesson Overview

Java application-এ অনেক class থাকে যেগুলোর কাজ খুব simple:

কিছু data hold করা
constructor দিয়ে initialize করা
accessor provide করা
equals() implement করা
hashCode() implement করা
toString() implement করা

Example:

final class CourseSummary {

    private final String code;
    private final String title;
    private final long priceInPaisa;

    CourseSummary(
            String code,
            String title,
            long priceInPaisa
    ) {
        this.code =
                code;

        this.title =
                title;

        this.priceInPaisa =
                priceInPaisa;
    }

    String code() {
        return code;
    }

    String title() {
        return title;
    }

    long priceInPaisa() {
        return priceInPaisa;
    }

    @Override
    public boolean equals(
            Object other
    ) {
        ...
    }

    @Override
    public int hashCode() {
        ...
    }

    @Override
    public String toString() {
        ...
    }
}

এই ধরনের value-oriented class-এর জন্য অনেক boilerplate লাগে।

Modern Java provides:

record

একই model:

record CourseSummary(
        String code,
        String title,
        long priceInPaisa
) {
}

অনেক concise।

কিন্তু record-এর purpose শুধু code ছোট করা নয়।

এটি একটি design statement:

এই type মূলত তার data values দিয়ে define হয়।

এই lesson-এ আমরা শিখব:

  • record কী
  • Record components
  • Generated constructor
  • Generated accessors
  • equals()
  • hashCode()
  • toString()
  • Record immutability
  • Shallow immutability
  • Compact constructor
  • Validation
  • Normal canonical constructor
  • Defensive copying
  • Records as value objects
  • Record methods
  • Static members
  • Record vs normal class
  • Record vs entity
  • DTO, command, result, key, value object
  • Records কোথায় useful
  • Records কোথায় inappropriate
  • Common mistakes

What Is a Record?

A record হলো Java-র একটি special class form যা data-oriented types conciseভাবে define করতে সাহায্য করে।

Example:

record CourseSummary(
        String code,
        String title,
        long priceInPaisa
) {
}

এই declaration থেকে Java automatically important members generate করে।

Conceptually:

components
constructor
accessors
equals()
hashCode()
toString()

Record Components

এই অংশ:

String code,
String title,
long priceInPaisa

হলো record components।

Full declaration:

record CourseSummary(
        String code,
        String title,
        long priceInPaisa
) {
}

Components define করে record-এর state।


Generated Constructor

এই record:

record CourseSummary(
        String code,
        String title,
        long priceInPaisa
) {
}

use করতে পারি:

CourseSummary summary =
        new CourseSummary(
                "JAVA",
                "Java Foundation",
                300_000
        );

Java automatically একটি canonical constructor provide করে যা সব components receive করে।


Generated Accessors

Normal JavaBean-style getter নয়:

getCode()
getTitle()
getPriceInPaisa()

Record accessor names component names-এর মতো।

Example:

summary.code();
summary.title();
summary.priceInPaisa();

Accessor Example

CourseSummary summary =
        new CourseSummary(
                "JAVA",
                "Java Foundation",
                300_000
        );

System.out.println(
        summary.code()
);

System.out.println(
        summary.title()
);

System.out.println(
        summary.priceInPaisa()
);

Records Are Final

A record implicitly final।

অর্থাৎ সাধারণভাবে:

class SpecialCourseSummary
        extends CourseSummary

এভাবে record extend করা যায় না।

Record value-oriented closed representation হিসেবে design করা।


Records Cannot Extend Arbitrary Classes

Record-এর superclass structure language-defined।

তাই record অন্য normal class extend করতে পারে না।

কিন্তু record interfaces implement করতে পারে।

Example:

record CourseCode(
        String value
) implements Comparable<CourseCode> {

    @Override
    public int compareTo(
            CourseCode other
    ) {
        return value.compareTo(
                other.value
        );
    }
}

Generated toString()

Example:

CourseSummary summary =
        new CourseSummary(
                "JAVA",
                "Java Foundation",
                300_000
        );

System.out.println(
        summary
);

Output conceptually:

CourseSummary[code=JAVA, title=Java Foundation, priceInPaisa=300000]

Exact component values visible থাকে।

Debugging-এর জন্য useful।


Generated equals()

Records value-based equality provide করে।

Example:

CourseSummary first =
        new CourseSummary(
                "JAVA",
                "Java Foundation",
                300_000
        );

CourseSummary second =
        new CourseSummary(
                "JAVA",
                "Java Foundation",
                300_000
        );

Then:

first.equals(
        second
)

returns:

true

কারণ component values equal।


This Is Different from Identity Equality

These are different objects:

first == second

will normally be:

false

কিন্তু:

first.equals(
        second
)

can be:

true

because records value semantics use করে।


Generated hashCode()

যেহেতু records automatically compatible equals() এবং hashCode() provide করে, তারা HashMap এবং HashSet keys হিসেবে খুব useful হতে পারে।

Example:

record EnrollmentKey(
        long learnerId,
        String courseCode
) {
}

Then:

Set<EnrollmentKey> enrollments =
        new HashSet<>();

Composite Key Example

EnrollmentKey key =
        new EnrollmentKey(
                1001,
                "JAVA"
        );

Another:

EnrollmentKey sameKey =
        new EnrollmentKey(
                1001,
                "JAVA"
        );

Because component values same:

key.equals(
        sameKey
)

is true।

এবং compatible hash code automatically generated।


Records and Value Semantics

Record-এর strongest use case:

value-like data

Examples:

CourseCode
EmailAddress
Money
DateRange
Coordinate
CourseSummary
EnrollmentKey
SearchCriteria
Command
Result

যেখানে object-এর identity-এর চেয়ে component values বেশি important।


Record Does Not Automatically Validate Data

Consider:

record CourseCode(
        String value
) {
}

This is valid:

new CourseCode(
        null
);

Unless we explicitly validate।

Record concise হলেও domain validation automatically আসে না।


Compact Constructor

Record validation-এর জন্য concise syntax আছে:

record CourseCode(
        String value
) {

    CourseCode {
        if (
                value == null
                || value.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }
    }
}

এটিকে বলা হয়:

compact constructor

Why Is It Called Compact?

Notice আমরা লিখিনি:

CourseCode(
        String value
)

again।

শুধু:

CourseCode {
    ...
}

Record header already component list জানে।


Automatic Assignment

Compact constructor-এ normally এই assignment manually লিখতে হয় না:

this.value =
        value;

Validation/body complete হওয়ার পরে record components-এর assignment automatically handled হয়।


Normalization Inside Compact Constructor

Suppose Course code:

" java "

কে normalize করে:

"JAVA"

করতে চাই।

record CourseCode(
        String value
) {

    CourseCode {
        if (
                value == null
                || value.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

        value =
                value.strip()
                        .toUpperCase();
    }
}

The normalized parameter value component হিসেবে stored হবে।


Use Locale When Case Normalization Matters

For machine/domain identifiers:

value.toUpperCase()

default locale-dependent হতে পারে।

More deliberate:

value =
        value.strip()
                .toUpperCase(
                        Locale.ROOT
                );

Import:

import java.util.Locale;

Strong CourseCode Value Object

import java.util.Locale;

record CourseCode(
        String value
) {

    CourseCode {
        if (
                value == null
                || value.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

        value =
                value.strip()
                        .toUpperCase(
                                Locale.ROOT
                        );

        if (
                !value.matches(
                        "[A-Z0-9-]+"
                )
        ) {
            throw new IllegalArgumentException(
                    "Invalid course code: "
                    + value
            );
        }
    }
}

Now:

new CourseCode(
        " java-basics "
);

stores:

JAVA-BASICS

Validation Creates an Invariant

Once CourseCode construction succeeds, downstream code can assume:

non-null
nonblank
normalized
valid format

This is much stronger than passing raw:

String

throughout the application।


Record as a Value Object

Compare:

String courseCode

with:

CourseCode courseCode

The second communicates domain meaning।

Method:

void enroll(
        String value
)

What is value?

Could be:

course code
email
title
token

Better:

void enroll(
        CourseCode courseCode
)

Type system now carries domain meaning।


Primitive Obsession

Using only:

String
long
int
boolean

for every domain concept can lead to:

Primitive Obsession

Example:

void enroll(
        long learnerId,
        String courseCode,
        long amount
)

Possible stronger types:

void enroll(
        LearnerId learnerId,
        CourseCode courseCode,
        Money amount
)

Records make small value types inexpensive to create।


ID Record Example

record LearnerId(
        long value
) {

    LearnerId {
        if (
                value <= 0
        ) {
            throw new IllegalArgumentException(
                    "Learner ID must be positive."
            );
        }
    }
}

Now method:

findLearner(
        LearnerId learnerId
)

cannot accidentally receive a raw Course ID without an explicit conversion/type mismatch।


Different IDs Should Be Different Types

Suppose both are long:

long learnerId;
long courseId;

Method:

enroll(
        courseId,
        learnerId
);

compiler may not detect reversed arguments if signature also uses two longs।

With:

LearnerId
CourseId

compiler helps।


Email Value Object

record EmailAddress(
        String value
) {

    EmailAddress {
        if (
                value == null
        ) {
            throw new IllegalArgumentException(
                    "Email is required."
            );
        }

        value =
                value.strip()
                        .toLowerCase(
                                Locale.ROOT
                        );

        if (
                value.isBlank()
                || value.contains(
                        " "
                )
                || !value.contains(
                        "@"
                )
        ) {
            throw new IllegalArgumentException(
                    "Invalid email address."
            );
        }
    }
}

This is a simplified email validation example।

The key lesson is:

validation belongs close to the value invariant

Custom Methods in a Record

Records can contain methods।

Example:

record Money(
        long amountInPaisa
) {

    Money {
        if (
                amountInPaisa < 0
        ) {
            throw new IllegalArgumentException(
                    "Amount cannot be negative."
            );
        }
    }

    Money add(
            Money other
    ) {
        return new Money(
                amountInPaisa
                + other.amountInPaisa
        );
    }

    boolean isZero() {
        return amountInPaisa
                == 0;
    }
}

Records Are Not Just Dumb DTOs

A record can have meaningful behavior।

Example:

money.add(
        otherMoney
);

This can be better than external utility:

MoneyUtils.add(
        first,
        second
);

if behavior naturally belongs to the value type।


Static Methods in Records

Records can also contain static methods।

Example:

record CourseCode(
        String value
) {

    static CourseCode of(
            String value
    ) {
        return new CourseCode(
                value
        );
    }
}

Use:

CourseCode code =
        CourseCode.of(
                "JAVA"
        );

Static Constants

Possible:

record Money(
        long amountInPaisa
) {

    static final Money ZERO =
            new Money(
                    0
            );
}

Instance Fields Beyond Components

Records are intended for state defined by their components।

You cannot freely add arbitrary mutable instance state like a normal class।

That restriction reinforces:

record state is the record components

Static fields are fine।


Records Are Shallowly Immutable

This is a critical point।

Suppose:

record Course(
        String title,
        List<String> topics
) {
}

The record component reference:

topics

cannot be reassigned inside the record after construction।

But the referenced List may still be mutable।


Example

List<String> topics =
        new ArrayList<>();

topics.add(
        "Java"
);

Course course =
        new Course(
                "Java Foundation",
                topics
        );

topics.add(
        "Backend"
);

Now:

course.topics()

may also contain:

Backend

because both reference the same mutable List।


Record Did Not Become Deeply Immutable

The record itself prevents replacing the component reference।

It does not automatically copy or freeze mutable objects stored inside।

This is why we call it:

shallow immutability

Defensive Copying

If we want Course topics protected:

record Course(
        String title,
        List<String> topics
) {

    Course {
        if (
                title == null
                || title.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Title is required."
            );
        }

        topics =
                List.copyOf(
                        topics
                );
    }
}

Why List.copyOf()?

Suppose caller passes mutable:

ArrayList

Record constructor stores a defensive copy।

Later caller mutates original list:

original.add(...)

record-এর internal list change হবে না।


Accessor Safety

List.copyOf() result unmodifiable।

So:

course.topics()
        .add(
                "Something"
        );

will fail rather than mutate record state।


Validate Before Copy

Better:

topics =
        Objects.requireNonNull(
                topics,
                "topics must not be null"
        );

topics =
        List.copyOf(
                topics
        );

Or:

topics =
        List.copyOf(
                Objects.requireNonNull(
                        topics
                )
        );

Elements Can Still Be Mutable

Even:

List.copyOf(...)

only protects the List structure।

Suppose:

List<MutableLesson>

contains mutable objects।

Those objects may still change।

So deep immutability requires all contained values to be suitably immutable too।


Prefer Immutable Components for Value Records

Records work especially well with:

String
primitive values
other immutable records
LocalDate
Instant
enum
List.copyOf(...) of immutable elements
Set.copyOf(...)
Map.copyOf(...)

Defensive Set Copy

record Course(
        String title,
        Set<String> tags
) {

    Course {
        tags =
                Set.copyOf(
                        tags
                );
    }
}

Defensive Map Copy

record Metadata(
        Map<String, String> values
) {

    Metadata {
        values =
                Map.copyOf(
                        values
                );
    }
}

Canonical Constructor

Instead of compact constructor, full canonical constructor লিখতে পারি।

Example:

record CourseCode(
        String value
) {

    CourseCode(
            String value
    ) {
        if (
                value == null
        ) {
            throw new IllegalArgumentException(
                    "Value is required."
            );
        }

        this.value =
                value.strip();
    }
}

এখানে parameter list explicitly লেখা এবং component field explicitly assign করা হয়।


Compact Constructor Is Often Cleaner

Same:

record CourseCode(
        String value
) {

    CourseCode {
        if (
                value == null
        ) {
            throw new IllegalArgumentException(
                    "Value is required."
            );
        }

        value =
                value.strip();
    }
}

For validation/normalization, compact form usually elegant।


Additional Constructors

Records can define additional constructors, but they must eventually initialize through the canonical representation।

Example:

record Money(
        long amountInPaisa
) {

    Money(
            int amountInTaka
    ) {
        this(
                amountInTaka * 100L
        );
    }
}

Now:

new Money(
        500
);

can represent:

50000 paisa

Be Careful with Ambiguous Units

Though overloaded constructors can be convenient, financial types should make units extremely clear।

Potentially clearer:

static Money fromTaka(
        long taka
) {
    return new Money(
            taka * 100
    );
}

than relying on:

new Money(
        500
)

where unit might be unclear।


Record Equality Includes All Components

Consider:

record Course(
        long id,
        String title
) {
}

Two records:

new Course(
        1,
        "Java"
)

and:

new Course(
        1,
        "Backend"
)

are not equal because title differs।

Record equality includes record components।


This Matters for Domain Entities

Suppose Course is an entity whose identity is:

CourseId

and title can change।

Should equality depend on:

id + title + description + status + lessons

?

Maybe not।

This is one reason not every domain entity should automatically become a record।


Value Object vs Entity

This distinction is very important।

Value Object

Defined mainly by its values।

Example:

CourseCode("JAVA")
Money(500000)
DateRange(start, end)

Two values with same components are logically same।

Records are excellent fit।


Entity

Has continuing identity over time।

Example:

Course ID 100

may remain the same Course even if:

title changes
status changes
lessons change

Entity identity and lifecycle can make a normal class more appropriate।


Example Entity

final class Course {

    private final CourseCode code;
    private String title;
    private CourseStatus status;

    Course(
            CourseCode code,
            String title
    ) {
        this.code =
                code;

        this.title =
                title;

        this.status =
                CourseStatus.DRAFT;
    }

    void changeTitle(
            String title
    ) {
        ...
    }

    void publish() {
        ...
    }
}

This has:

lifecycle
state transitions
encapsulated mutation
business behavior

A normal class may communicate that model better।


Record Does Not Mean "Immutable Domain Entity"

Records naturally suit immutable state representation।

If an object has meaningful state transitions:

DRAFT
→ PUBLISHED
→ ARCHIVED

and behavior controls those transitions, forcing it into a record can make design awkward।


Records for DTOs

A DTO or summary is a strong use case।

Example:

record CourseSummary(
        String code,
        String title,
        long priceInPaisa,
        CourseStatus status
) {
}

It simply describes a snapshot of data।


Records for Commands

Example:

record CreateCourseCommand(
        String code,
        String title,
        long priceInPaisa
) {
}

A command captures input data for an operation।

Validation can live in command or deeper domain types depending on architecture।


Records for Results

Example:

record EnrollmentResult(
        long enrollmentId,
        String courseCode,
        String learnerEmail
) {
}

Useful immutable output representation।


Records for Keys

Excellent:

record EnrollmentKey(
        LearnerId learnerId,
        CourseCode courseCode
) {
}

Because value equality + hashing automatically align with components।


Records for Events

Example:

record CoursePublished(
        CourseCode courseCode,
        Instant publishedAt
) {
}

Represents an immutable fact।

Very natural fit।


Records with Interfaces

Example:

interface DomainEvent {

    Instant occurredAt();
}

Record:

record CoursePublished(
        CourseCode courseCode,
        Instant occurredAt
) implements DomainEvent {
}

Records can participate in abstractions।


Record as Map Key

Suppose we need uniqueness by:

learner + course
record EnrollmentKey(
        long learnerId,
        String courseCode
) {
}

Then:

Map<EnrollmentKey, Enrollment> enrollments =
        new HashMap<>();

Lookup:

EnrollmentKey key =
        new EnrollmentKey(
                learnerId,
                courseCode
        );

Enrollment enrollment =
        enrollments.get(
                key
        );

No manual equals()/hashCode() implementation required।


Records and Hashing

Recall hashing rule:

equals() true
→ hashCode() same

Records generate component-based equality/hash behavior consistently।

This makes them strong candidates for immutable hash keys।

Still:

components themselves should have stable equality

for safe use।


Mutable Component as Hash Key Risk

Suppose:

record Key(
        List<String> values
) {
}

and mutable List is used directly।

If list contents change after key goes into HashMap, its hash/equality behavior can effectively change।

Even though outer record cannot reassign values, inner object is mutable।

Therefore:

record alone does not make a safe hash key

Defensive copy matters।


Better

record Key(
        List<String> values
) {

    Key {
        values =
                List.copyOf(
                        values
                );
    }
}

Now collection structure is stable।


Overriding Accessors

Records allow explicit accessor methods, but use carefully।

Example:

record Name(
        String value
) {

    @Override
    public String value() {
        return value;
    }
}

This adds nothing।

Avoid unnecessary overrides।


Do Not Surprise the Caller

Bad idea conceptually:

record Price(
        long amount
) {

    public long amount() {
        return amount * 2;
    }
}

Now accessor no longer simply represents stored component semantics।

That defeats record transparency।

If transformation needed, use another method:

long doubledAmount() {
    return amount * 2;
}

Record Component Names Matter

Because accessor names come directly from component names:

record Course(
        String c,
        String t
) {
}

produces:

c()
t()

Poor API।

Prefer:

record CourseSummary(
        String code,
        String title
) {
}

Then:

code()
title()

communicate meaning।


Records and Null

Records do not automatically reject null।

Example:

record Person(
        String name
) {
}

This compiles:

new Person(
        null
);

If null invalid:

record Person(
        String name
) {

    Person {
        name =
                Objects.requireNonNull(
                        name,
                        "name must not be null"
                );
    }
}

Normalize Strings at Construction Boundary

Suppose code should always be trimmed।

Better:

record CourseCode(
        String value
) {

    CourseCode {
        value =
                Objects.requireNonNull(
                        value
                ).strip();

        if (
                value.isEmpty()
        ) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }
    }
}

Now downstream code does not repeatedly call:

strip()

Validate Once, Trust the Type

This is a powerful value-object principle:

Validate at construction
↓
valid object created
↓
downstream code trusts invariant

Instead of:

if (
        courseCode != null
        && !courseCode.isBlank()
)

everywhere।


Strong Types Reduce Repeated Validation

Raw String version:

void publish(
        String courseCode
) {
    if (
            courseCode == null
            || courseCode.isBlank()
    ) {
        ...
    }
}

Another method repeats same validation।

With:

CourseCode

constructor validates once।

Method:

void publish(
        CourseCode courseCode
) {
    ...
}

can trust code validity।


Record vs Normal Class

Use a record when object is primarily:

a value
a snapshot
a result
a command
a key
a small immutable data model

Use a normal class when object needs:

complex lifecycle
controlled mutable state
identity semantics different from all fields
rich encapsulation
inheritance from a class
internal state not represented directly by constructor data

Record Is Not "Better Class"

It is a different design tool।

Bad reasoning:

Records are modern
therefore every class should become a record.

Correct reasoning:

Does this type have value-oriented semantics?

Example: Good Record

record DateRange(
        LocalDate start,
        LocalDate end
) {

    DateRange {
        Objects.requireNonNull(
                start
        );

        Objects.requireNonNull(
                end
        );

        if (
                end.isBefore(
                        start
                )
        ) {
            throw new IllegalArgumentException(
                    "End date cannot be before start date."
            );
        }
    }

    boolean contains(
            LocalDate date
    ) {
        return !date.isBefore(
                start
        )
                && !date.isAfter(
                        end
                );
    }
}

This is an excellent value object।


Why DateRange Fits Record Well

Its identity is exactly:

start
end

Two ranges with same dates are logically equal।

It can be immutable।

Behavior derives naturally from its values।


Example: Good Result Record

record CourseStatistics(
        long totalCourses,
        long publishedCourses,
        long archivedCourses
) {
}

Pure immutable result।


Example: Good Request Record

record RegisterLearnerRequest(
        String name,
        String email
) {
}

Though boundary validation still needs appropriate design।


Example: Maybe Not a Record

Course

with:

addLesson()
changeTitle()
publish()
archive()

If these state transitions are core domain behavior, normal class may be more expressive।


Entity Snapshot Can Still Be a Record

The entity itself may be a normal class:

Course

but read model:

record CourseSummary(...)

can be a record।

This separation is common and useful।


Example

Domain entity:

final class Course {
    ...
}

Projection:

record CourseSummary(
        String code,
        String title,
        CourseStatus status,
        int lessonCount
) {
}

The summary is a snapshot, not the mutable lifecycle owner।


Records and Serialization

Many frameworks and libraries support records well, but framework compatibility is a separate concern।

Do not assume every old library automatically understands every modern Java feature।

In plain Java code, records are first-class language types।

At external integration boundaries, verify framework support when needed।


Records and Persistence

Do not choose:

record vs class

solely based on database mapping convenience।

First decide domain semantics।

Then adapt persistence boundary appropriately।


Record with Nested Records

record CourseSummary(
        CourseCode code,
        String title,
        Money price
) {
}

This is often stronger than:

record CourseSummary(
        String code,
        String title,
        long price
) {
}

because domain concepts remain typed।


Complete Example

import java.util.List;
import java.util.Locale;
import java.util.Objects;

public class Main {

    public static void main(String[] args) {
        CourseCode code =
                new CourseCode(
                        " java-foundation "
                );

        CourseSummary first =
                new CourseSummary(
                        code,
                        "Java Foundation",
                        300_000,
                        List.of(
                                "Java",
                                "OOP"
                        )
                );

        CourseSummary second =
                new CourseSummary(
                        new CourseCode(
                                "JAVA-FOUNDATION"
                        ),
                        "Java Foundation",
                        300_000,
                        List.of(
                                "Java",
                                "OOP"
                        )
                );

        System.out.println(
                first
        );

        System.out.println(
                first.equals(
                        second
                )
        );

        System.out.println(
                first.code()
        );
    }

    record CourseCode(
            String value
    ) {

        CourseCode {
            value =
                    Objects.requireNonNull(
                            value,
                            "Course code is required."
                    )
                            .strip()
                            .toUpperCase(
                                    Locale.ROOT
                            );

            if (
                    value.isBlank()
                    || !value.matches(
                            "[A-Z0-9-]+"
                    )
            ) {
                throw new IllegalArgumentException(
                        "Invalid course code: "
                        + value
                );
            }
        }

        @Override
        public String toString() {
            return value;
        }
    }

    record CourseSummary(
            CourseCode code,
            String title,
            long priceInPaisa,
            List<String> topics
    ) {

        CourseSummary {
            Objects.requireNonNull(
                    code,
                    "Course code is required."
            );

            title =
                    Objects.requireNonNull(
                            title,
                            "Course title is required."
                    ).strip();

            if (
                    title.isBlank()
            ) {
                throw new IllegalArgumentException(
                        "Course title is required."
                );
            }

            if (
                    priceInPaisa < 0
            ) {
                throw new IllegalArgumentException(
                        "Price cannot be negative."
                );
            }

            topics =
                    List.copyOf(
                            Objects.requireNonNull(
                                    topics,
                                    "Topics are required."
                            )
                    );
        }
    }
}

What This Example Demonstrates

CourseCode:

normalizes itself
validates itself
has value equality
can safely represent the domain concept

CourseSummary:

is a snapshot
has immutable component references
defensively copies topics
gets equals/hashCode/toString automatically

This is a strong record use case।


Common Mistake 1 — Assuming Record Means Deeply Immutable

Wrong।

record Something(
        List<String> values
) {
}

does not make values immutable automatically।

Use defensive copying when required।


Common Mistake 2 — Using Record for Every Entity

A lifecycle-rich mutable entity may be better as a normal class।

Do not choose record solely because it is concise।


Common Mistake 3 — No Validation

record CourseCode(
        String value
) {
}

allows invalid values unless validation added।

Records do not automatically enforce domain rules।


Common Mistake 4 — Exposing Mutable Collections

record Course(
        List<String> topics
) {
}

with caller-owned mutable List can break value stability।

Consider:

List.copyOf(...)

Common Mistake 5 — Poor Component Names

Avoid:

record X(
        String a,
        int b
) {
}

when this is public/domain code।

Component names become API names।


Common Mistake 6 — Treating Record Accessors Like Bean Getters

Record:

record Course(
        String title
) {
}

accessor:

course.title()

not automatically:

course.getTitle()

Common Mistake 7 — Overriding Generated Methods Without Need

Generated:

equals()
hashCode()
toString()

are usually exactly why record is useful।

Override only when semantics genuinely require it and remain consistent with design।


Common Mistake 8 — Making Equality Semantics Wrong for an Entity

If object identity is only:

id

but record components include many mutable/changeable attributes, component-based equality may not match entity semantics।

Use a normal class when appropriate।


Common Mistake 9 — Mutable Record Components as Hash Keys

Outer record may appear immutable, but inner mutable collection/object can change equality/hash behavior।

Use stable immutable components for hash keys।


Common Mistake 10 — Using Raw Primitives When Strong Types Help

Instead of:

String email
String courseCode
long learnerId

consider small validated record value objects when they reduce ambiguity and repeated validation।


Practice 1 — Basic Record

Create a record:

Point
x
y

Solution

record Point(
        int x,
        int y
) {
}

Practice 2 — Accessor

Given:

Point point =
        new Point(
                10,
                20
        );

How do you access x?

Answer

point.x()

Practice 3 — Equality

new Point(
        10,
        20
).equals(
        new Point(
                10,
                20
        )
)

Result?

Answer

true

because record component values are equal।


Practice 4 — Validation

Create positive CourseId

Solution

record CourseId(
        long value
) {

    CourseId {
        if (
                value <= 0
        ) {
            throw new IllegalArgumentException(
                    "Course ID must be positive."
            );
        }
    }
}

Practice 5 — Defensive Copy

Given:

record Tags(
        List<String> values
)

protect collection structure।

Solution

record Tags(
        List<String> values
) {

    Tags {
        values =
                List.copyOf(
                        values
                );
    }
}

Practice 6 — Record or Class?

Type:

Money
amount
currency
immutable value semantics

Answer

Record is a strong candidate।


Practice 7 — Record or Class?

Type:

Course
has mutable lifecycle
addLesson()
publish()
archive()
changeTitle()

Answer

A normal class is likely a better starting point।


Practice 8 — Record or Class?

Type:

CourseSummary
code
title
price
lessonCount

Read-only snapshot।

Answer

Record is an excellent fit।


Practice 9 — Null Validation

Does:

record Email(
        String value
) {
}

automatically reject null?

Answer

No।

Explicit validation required।


Practice 10 — Shallow Immutability

Is this deeply immutable?

record Data(
        ArrayList<String> values
) {
}

Answer

No।

The ArrayList itself can still mutate।


Practice 11 — Map Key

Why can:

record EnrollmentKey(
        long learnerId,
        String courseCode
) {
}

be convenient as a HashMap key?

Answer

Because record automatically provides component-based:

equals()
hashCode()

which aligns naturally with value-key semantics।


Practice 12 — Component Names

Which is better?

record Course(
        String c,
        String t
)

or:

record CourseSummary(
        String code,
        String title
)

Answer

The second।

Record component names become part of the API through accessors।


Practice 13 — Strong Type

Why might:

CourseCode

be better than raw:

String

Answer

It can encode:

domain meaning
validation
normalization
type safety

once, instead of repeating them throughout the codebase।


Practice 14 — Compact Constructor

What does this do?

record Name(
        String value
) {

    Name {
        value =
                value.strip();
    }
}

Answer

Constructor parameter value normalize করে এবং normalized value record component হিসেবে stored হয়।


Practice 15 — Value Object or Entity?

Two objects with same values should logically be interchangeable।

What concept is this closer to?

Answer

Value Object

Records fit this style well।


True or False

  1. A record is a special form of Java class.
  2. Records automatically generate accessors.
  3. Record accessors are normally named getX().
  4. Records automatically provide value-based equals().
  5. Records automatically provide compatible hashCode().
  6. Records automatically reject null values.
  7. Records can contain validation.
  8. Compact constructors are useful for validation and normalization.
  9. Record components are deeply immutable automatically.
  10. List.copyOf() can help protect collection structure.
  11. Records are excellent candidates for many value objects.
  12. Every domain entity should be a record.
  13. Records can implement interfaces.
  14. Records can contain methods.
  15. Record component names are part of the public API.

Answers

1. True
2. True
3. False
4. True
5. True
6. False
7. True
8. True
9. False
10. True
11. True
12. False
13. True
14. True
15. True

Knowledge Check

Question 1

record কী ধরনের problem solve করে?

Question 2

Record component কী?

Question 3

Record automatically কোন common methods provide করে?

Question 4

Compact constructor কেন useful?

Question 5

Record কেন deeply immutable নয়?

Question 6

Defensive copying কী?

Question 7

Value Object কী?

Question 8

Entity এবং Value Object-এর মূল conceptual difference কী?

Question 9

কেন record composite Map key হিসেবে useful?

Question 10

কেন required domain validation record constructor-এর মধ্যে রাখা useful হতে পারে?

Question 11

কখন normal class record-এর চেয়ে better?

Question 12

Strong domain record raw primitive type-এর চেয়ে কী advantage দিতে পারে?


Knowledge Check Answers

Answer 1

Value-oriented data class-এর repetitive boilerplate কমায় এবং component-based state/equality conciseভাবে express করে।

Answer 2

Record header-এর declared state values।

Example:

record Course(
        String code,
        String title
)

এখানে:

code
title

record components।

Answer 3

Record সাধারণভাবে component-based:

canonical constructor
accessors
equals()
hashCode()
toString()

provide করে।

Answer 4

Compact constructor full constructor parameter list এবং field assignments repeat না করে validation এবং normalization করতে সাহায্য করে।

Answer 5

Record component references stable হলেও referenced objects mutable হতে পারে।

Example:

List<String>

component-এর list contents change হতে পারে যদি defensive copy না করা হয়।

Answer 6

Caller-provided mutable object-এর independent copy store করা, যাতে caller পরে original object mutate করলেও internal value change না হয়।

Example:

List.copyOf(
        values
)

Answer 7

এমন domain object যার logical identity তার values দ্বারা define হয়।

Same values সাধারণত same logical value represent করে।

Answer 8

Value Object:

value দ্বারা identity

Entity:

continuing identity থাকে
attributes সময়ের সাথে change করতে পারে

Answer 9

কারণ record automatically consistent component-based equals() এবং hashCode() provide করে।

Composite key semantics-এর সাথে এটি naturally fit করে।

Answer 10

একবার valid record construct হলে downstream code invariant trust করতে পারে এবং repeated validation কমে।

Answer 11

যখন object-এর:

mutable lifecycle
controlled state transitions
identity semantics
complex encapsulation

important।

Answer 12

Strong record type:

meaning encode করে
invalid state reject করতে পারে
normalization centralize করে
wrong argument type mix-up কমায়

Practical Record Selection Guide

Use a record for:

Value Object
DTO
Request
Command
Result
Summary
Composite Key
Immutable Event
Configuration snapshot
Small immutable model

Consider a normal class for:

Rich domain entity
Mutable lifecycle
State transitions
Identity independent of all fields
Complex internal invariants
Encapsulated mutable collections
Long-lived behavioral object

Value Object Examples

record CourseCode(
        String value
) {
}
record LearnerId(
        long value
) {
}
record EmailAddress(
        String value
) {
}
record DateRange(
        LocalDate start,
        LocalDate end
) {
}
record EnrollmentKey(
        LearnerId learnerId,
        CourseCode courseCode
) {
}

Record Design Checklist

একটি নতুন record বানানোর আগে জিজ্ঞেস করুন:

এই type কি তার values দিয়ে define হয়?

সব components কি constructor সময়েই known?

State কি ideally immutable?

Component-based equality কি correct?

Validation construction-এর সময় করা যায়?

Mutable collections defensive copy করা হয়েছে?

Component names clear কি?

এই type-এর lifecycle mutation দরকার কি?

এটি entity না value object?

Strong type বানালে primitive ambiguity কমবে কি?

Core Mental Model

Record-কে শুধু:

short class

হিসেবে ভাববেন না।

Better:

Record declares:
"This type is primarily a value
defined by these components."

Example:

record CourseCode(
        String value
) {
}

means:

CourseCode-এর meaning তার value।

আর:

record CourseSummary(
        CourseCode code,
        String title,
        long priceInPaisa
) {
}

means:

This is a data snapshot
described by these components.

কিন্তু:

A Course that changes title,
adds lessons,
publishes,
archives

একটি lifecycle-rich entity।

সেটিকে normal class হিসেবে model করা অনেক সময় better।


Lesson Summary

এই lesson-এ আমরা Java record এবং modern data modeling-এর foundation শিখেছি।

আমরা শিখেছি:

  • record value-oriented class declaration concise করে
  • Record components type-এর state define করে
  • Java canonical constructor generate করতে পারে
  • Record accessor component name ব্যবহার করে
  • Records automatically equals(), hashCode(), এবং toString() provide করে
  • Record equality component values-এর উপর ভিত্তি করে
  • Records HashMap/HashSet keys হিসেবে useful হতে পারে
  • Records automatically validate data করে না
  • Compact constructor validation এবং normalization-এর জন্য useful
  • Small value objects record হিসেবে excellent fit
  • Strong domain types raw primitives-এর ambiguity কমাতে পারে
  • Records methods এবং static members রাখতে পারে
  • Records interfaces implement করতে পারে
  • Records shallowly immutable
  • Mutable components এখনও mutate হতে পারে
  • List.copyOf(), Set.copyOf(), Map.copyOf() defensive copying-এ useful
  • Value Object এবং Entity একই concept নয়
  • Value Object-এর identity values দিয়ে define হয়
  • Entity-এর continuing identity এবং lifecycle থাকে
  • DTO, command, result, summary, key, event-এর জন্য records strong fit
  • Lifecycle-rich mutable entity-এর জন্য normal class often better
  • Record component names API design-এর অংশ
  • Record modern বলে সব class record করা উচিত নয়
  • Type-এর semantics design choice determine করবে

সবচেয়ে important principle:

Record ব্যবহার করুন
যখন object মূলত একটি value।

Normal class ব্যবহার করুন
যখন object মূলত একটি lifecycle এবং behavior-এর owner।

Next Lesson

পরবর্তী lesson:

Introduction to Concurrency and Thread Safety

আমরা শিখব:

  • Process এবং Thread
  • Concurrency কী
  • Parallelism কী
  • Concurrency এবং Parallelism-এর difference
  • Shared mutable state
  • Race condition
  • Thread safety
  • Atomic-looking code কেন সবসময় atomic নয়
  • synchronized
  • Critical section
  • Visibility
  • volatile-এর basic concept
  • Immutability এবং thread safety
  • Stateless code
  • Thread confinement
  • কেন concurrency bugs reproduce করা কঠিন
  • Safe concurrency design-এর foundation