Object-Oriented Programming Foundations

Designing Immutable Objects

ReadingPreview

You are viewing a free preview lesson.

Lesson Overview

Mutable object-এর state object creation-এর পরে পরিবর্তন করা যায়।

course.publish();
enrollment.completeLessons(3);

এ ধরনের পরিবর্তন অনেক domain object-এর জন্য natural।

কিন্তু সব objects-এর state পরিবর্তনশীল হওয়া প্রয়োজন নেই।

Examples:

Course code
Email address
Money amount
Geographic coordinate
Date range
Enrollment ID

একটি course code যদি হয়:

JAVA-FOUNDATION

তাহলে একই object পরে অন্য code represent করা confusing:

BACKEND-DEVELOPMENT

এ ধরনের value একবার তৈরি হওয়ার পরে অপরিবর্তিত থাকলে code বুঝতে, validate করতে এবং safely share করতে সহজ হয়।

যে object creation-এর পরে observable state পরিবর্তন করতে দেয় না, তাকে immutable object বলা হয়।

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

  • Mutable এবং immutable object-এর পার্থক্য
  • final fields
  • Constructor-only initialization
  • Setters বাদ দেওয়া
  • Immutable value objects
  • Shallow এবং deep immutability
  • Defensive copying
  • Immutable collections
  • Immutability-এর practical benefits
  • কোথায় immutability appropriate
  • কোথায় controlled mutability প্রয়োজন

Learning Objectives

এই lesson শেষ করার পর আপনি পারবেন:

  • Immutable object কী ব্যাখ্যা করতে
  • final field-এর behavior বুঝতে
  • Constructor দিয়ে complete immutable state তৈরি করতে
  • Setter ছাড়া value object design করতে
  • Mutable internal object safely handle করতে
  • Defensive copy ব্যবহার করতে
  • Shallow এবং deep immutability আলাদা করতে
  • Immutable value object-এর equality design করতে
  • Immutability এবং thread safety-এর relationship বুঝতে
  • Entity এবং value object-এর জন্য appropriate mutability নির্বাচন করতে

Mutable Objects

Mutable object-এর state creation-এর পরে পরিবর্তন করা যায়।

public class Course {

    private String title;
    private boolean published;

    public boolean changeTitle(
            String newTitle
    ) {
        if (
                newTitle == null
                || newTitle.isBlank()
        ) {
            return false;
        }

        title =
                newTitle.strip();

        return true;
    }

    public boolean publish() {
        if (published) {
            return false;
        }

        published = true;

        return true;
    }
}

এই object mutable কারণ:

course.changeTitle(...);
course.publish();

তার state পরিবর্তন করে।

Mutable হওয়া automatically bad design নয়।

Course publication status বা enrollment progress naturally পরিবর্তন হতে পারে।


Immutable Objects

Immutable object creation-এর পরে নিজের observable state পরিবর্তন করতে দেয় না।

Example:

public final class CourseCode {

    private final String value;

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

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

    public String getValue() {
        return value;
    }
}

Object creation:

CourseCode code =
        new CourseCode(
                "java-foundation"
        );

Stored value:

JAVA-FOUNDATION

এরপর code change করার কোনো method নেই।

code.setValue(...);
code.changeValue(...);

এ ধরনের API expose করা হয়নি।


What Makes an Object Immutable?

একটি class immutable design করতে সাধারণত প্রয়োজন:

  1. Fields private
  2. Fields যতটা সম্ভব final
  3. Constructor complete state initialize করবে
  4. কোনো setter থাকবে না
  5. State-changing method থাকবে না
  6. Mutable internal object direct expose করা হবে না
  7. Mutable constructor argument defensiveভাবে copy করা হবে
  8. Class inheritance-এর মাধ্যমে mutability introduce করা prevent করতে class final করা যেতে পারে

final Fields

A final field একবার assign হওয়ার পরে reassign করা যায় না।

private final String value;

Constructor-এ assignment:

this.value =
        value;

পরে invalid:

this.value =
        "NEW-VALUE";

Compiler error হবে।


final Must Be Initialized

Blank final field:

private final String code;

Constructor-এর প্রতিটি valid execution path-এ initialize করতে হবে।

public CourseCode(
        String code
) {
    this.code =
            code;
}

যদি কোনো execution path field assign না করে, code compile নাও করতে পারে।


final Reference Does Not Make the Object Immutable

Consider:

private final List<String> lessons;

final means:

lessons reference cannot point to another List

কিন্তু referenced list mutable হলে:

lessons.add(
        "New Lesson"
);

still possible।

Important distinction:

final reference reassign prevent করে; referenced object-এর internal mutation prevent করে না।


Shallow Immutability

public final class CoursePlan {

    private final List<String> lessons;

    public CoursePlan(
            List<String> lessons
    ) {
        this.lessons =
                lessons;
    }

    public List<String> getLessons() {
        return lessons;
    }
}

Fields final হলেও class সত্যিকার অর্থে immutable নয়।

Caller:

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

lessonTitles.add(
        "Introduction"
);

CoursePlan plan =
        new CoursePlan(
                lessonTitles
        );

lessonTitles.add(
        "Classes and Objects"
);

plan-এর internal stateও পরিবর্তিত হয়েছে, কারণ same list reference share করছে।


Mutation Through a Getter

Caller আরও করতে পারে:

plan.getLessons()
        .clear();

Getter internal mutable list direct return করেছে।

Encapsulation bypass হয়েছে।


Defensive Copying

Constructor argument copy করে internal state protect করা যায়।

public CoursePlan(
        List<String> lessons
) {
    if (lessons == null) {
        throw new IllegalArgumentException(
                "Lessons are required."
        );
    }

    this.lessons =
            List.copyOf(
                    lessons
            );
}

List.copyOf() একটি unmodifiable copy তৈরি করে।

এখন caller original list modify করলেও CoursePlan state change হবে না।


Returning an Immutable View or Copy

Getter:

public List<String> getLessons() {
    return lessons;
}

যদি constructor List.copyOf() ব্যবহার করে immutable list store করে, returned list caller modify করতে পারবে না।

plan.getLessons()
        .add("Another Lesson");

Runtime-এ:

UnsupportedOperationException

আরও explicitভাবে:

public List<String> getLessons() {
    return List.copyOf(
            lessons
    );
}

Internal collection already immutable হলে repeated copy সবসময় প্রয়োজন নাও হতে পারে।


Immutable Elements Matter Too

Suppose:

private final List<Lesson> lessons;

List unmodifiable হলেও Lesson objects mutable হতে পারে।

plan.getLessons()
        .get(0)
        .changeTitle(
                "Changed"
        );

Collection structure change হয়নি, কিন্তু nested object change হয়েছে।

Deep immutability-এর জন্য contained elements-ও immutable হতে হবে অথবা defensive copies প্রয়োজন।


Shallow vs Deep Immutability

Shallow Immutability

Object-এর own fields reassign করা যায় না, কিন্তু referenced objects mutate হতে পারে।

Deep Immutability

Object graph-এর externally observable কোনো অংশই mutation allow করে না।

Deep immutability অর্জন করতে:

  • Nested objects immutable হতে পারে
  • Collections immutable হতে পারে
  • Mutable inputs copy করতে হয়
  • Mutable outputs direct expose করা যাবে না

String Helps Immutability

String immutable।

private final String value;

Getter:

public String getValue() {
    return value;
}

safe, কারণ caller String object-এর content modify করতে পারে না।

String operation নতুন value তৈরি করে।

String upper =
        value.toUpperCase();

Original value unchanged থাকে।


Immutable Value Objects

Value object সাধারণত identity-এর পরিবর্তে value দ্বারা defined।

Examples:

CourseCode
EmailAddress
Money
DateRange
Coordinate

দুইটি CourseCode object same normalized value ধারণ করলে logically equal হওয়া উচিত।

CourseCode first =
        new CourseCode(
                "java-foundation"
        );

CourseCode second =
        new CourseCode(
                "JAVA-FOUNDATION"
        );

Expected:

first.equals(second)
true

Complete Immutable CourseCode

import java.util.Objects;

public final class CourseCode {

    private final String value;

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

        String normalizedValue =
                value.strip()
                        .toUpperCase();

        if (
                !normalizedValue.matches(
                        "[A-Z0-9-]+"
                )
        ) {
            throw new IllegalArgumentException(
                    "Course code may contain only letters, numbers, and hyphens."
            );
        }

        this.value =
                normalizedValue;
    }

    public String getValue() {
        return value;
    }

    @Override
    public boolean equals(
            Object other
    ) {
        if (this == other) {
            return true;
        }

        if (
                other == null
                || getClass()
                != other.getClass()
        ) {
            return false;
        }

        CourseCode courseCode =
                (CourseCode) other;

        return value.equals(
                courseCode.value
        );
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                value
        );
    }

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

Why the Class Is final

public final class CourseCode

Class final হলে অন্য class এটিকে extend করতে পারবে না।

Without final, subclass additional mutable state বা behavior introduce করতে পারে।

class MutableCourseCode
        extends CourseCode {
}

Immutable base class-এর contract দুর্বল হতে পারে।

final immutable class-এর design intent protect করে।


Constructor Establishes the Complete Value

public CourseCode(
        String value
)

Constructor:

  • Required input নেয়
  • Input validate করে
  • Input normalize করে
  • Final field initialize করে

Object creation-এর পরে কোনো intermediate state নেই।

null
blank
partially normalized

কোনোটিই successfully created object-এর state হতে পারে না।


No Setter

There is intentionally no:

setValue()
changeValue()

Course code change দরকার হলে নতুন object তৈরি করতে হবে।

CourseCode newCode =
        new CourseCode(
                "BACKEND-DEVELOPMENT"
        );

Immutable object update মানে সাধারণত replacement value তৈরি করা।


Using Immutable Objects in Mutable Entities

একটি Course entity mutable হতে পারে, কিন্তু তার code immutable value object হতে পারে।

public class Course {

    private final CourseCode code;

    private String title;
    private boolean published;

    public Course(
            CourseCode code,
            String title
    ) {
        if (code == null) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

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

        this.code = code;
        this.title = title.strip();
        this.published = false;
    }

    public boolean changeTitle(
            String newTitle
    ) {
        if (
                newTitle == null
                || newTitle.isBlank()
        ) {
            return false;
        }

        title =
                newTitle.strip();

        return true;
    }

    public boolean publish() {
        if (published) {
            return false;
        }

        published = true;

        return true;
    }

    public CourseCode getCode() {
        return code;
    }

    public String getTitle() {
        return title;
    }

    public boolean isPublished() {
        return published;
    }
}

এই design-এ:

Course entity mutable
CourseCode value immutable

এটি common এবং practical combination।


Immutable Does Not Mean Every Field Must Be Primitive

Immutable object অন্য immutable objects reference করতে পারে।

public final class EnrollmentIdentity {

    private final long learnerId;
    private final CourseCode courseCode;

    public EnrollmentIdentity(
            long learnerId,
            CourseCode courseCode
    ) {
        if (learnerId <= 0) {
            throw new IllegalArgumentException(
                    "Learner ID must be positive."
            );
        }

        if (courseCode == null) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

        this.learnerId = learnerId;
        this.courseCode = courseCode;
    }
}

CourseCode immutable হওয়ায় reference safely share করা যায়।


Benefits of Immutability

Easier Reasoning

একবার object inspect করলে জানেন state unexpectedly change হবে না।

CourseCode code =
        course.getCode();

অন্য method code object mutate করবে না।


Safer Sharing

একই immutable object multiple objects share করতে পারে।

Enrollment first =
        new Enrollment(
                nur,
                course
        );

Enrollment second =
        new Enrollment(
                jalisa,
                course
        );

Immutable value object shared হলে synchronization concern কমে।


Thread Safety

Immutable object-এর state change হয় না।

Multiple threads একই object read করলে mutation race থাকে না।

তাই properly immutable objects inherently thread-safe for read-only use।

তবে immutable object external mutable dependency use করলে পুরো operation thread-safe হবে—এমন নয়।


Stable Equality and Hash Code

Equality fields immutable হলে object-এর hashCode() lifetime-এর মধ্যে change হয় না।

Hash-based collections-এর জন্য এটি safer।


Easier Testing

Input object method call-এর মধ্যে silently change হবে না।

Expected result reason করা সহজ।


Safe Caching

Immutable values cache এবং reuse করা comparatively safe।

Same object future caller mutate করতে পারবে না।


Costs and Trade-Offs

Immutability free নয়।

New Objects Must Be Created

State variation represent করতে নতুন object দরকার।

Money increased =
        original.add(
                additional
        );

Original unchanged।

High-frequency update scenario-তে allocation বাড়তে পারে, যদিও JVM অনেক allocation optimize করতে পারে।


Large Object Graph Copying

Large mutable structures deeply copy করা expensive হতে পারে।

Persistent data structures বা controlled mutation better হতে পারে।


Some Domains Are Naturally Stateful

Enrollment progress naturally changes।

0% → 20% → 80% → 100%

প্রতিটি lesson completion-এ entirely new enrollment object তৈরি করা possible, কিন্তু beginner domain model-এ controlled mutable entity simpler হতে পারে।


Immutability Is Not an Absolute Rule

Avoid simplistic rule:

Every object must be immutable.

Better:

Prefer immutability for values and stable configuration; use controlled mutability where state transitions are central to the domain.

Likely immutable:

CourseCode
EmailAddress
Money
DateRange
Coordinates

Likely controlled mutable:

Course publication status
Enrollment progress
Shopping cart
Order workflow
Account state

Entity vs Value Object

Entity

Identity গুরুত্বপূর্ণ।

State সময়ের সঙ্গে change হতে পারে।

Example:

Course
Learner
Enrollment

A course title change হলেও একই course থাকতে পারে।

Value Object

Value গুরুত্বপূর্ণ।

Same values মানে logically same value।

Example:

CourseCode
Money
EmailAddress

Value change মানে নতুন value object।


Immutable Money Example

Money floating-point দিয়ে model করা risky।

Immutable money value object minor units ব্যবহার করতে পারে।

import java.util.Objects;

public final class Money {

    private final long amountInPaisa;

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

        this.amountInPaisa =
                amountInPaisa;
    }

    public Money add(
            Money other
    ) {
        if (other == null) {
            throw new IllegalArgumentException(
                    "Other amount is required."
            );
        }

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

    public Money applyDiscount(
            int percentage
    ) {
        if (
                percentage < 0
                || percentage > 100
        ) {
            throw new IllegalArgumentException(
                    "Discount must be between 0 and 100."
            );
        }

        long discount =
                amountInPaisa
                * percentage
                / 100;

        return new Money(
                amountInPaisa
                - discount
        );
    }

    public long getAmountInPaisa() {
        return amountInPaisa;
    }

    @Override
    public boolean equals(
            Object other
    ) {
        if (this == other) {
            return true;
        }

        if (
                other == null
                || getClass()
                != other.getClass()
        ) {
            return false;
        }

        Money money =
                (Money) other;

        return amountInPaisa
                == money.amountInPaisa;
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                amountInPaisa
        );
    }

    @Override
    public String toString() {
        return "BDT "
                + amountInPaisa
                / 100.0;
    }
}

Immutable Operations Return New Objects

Money originalPrice =
        new Money(
                499_000L
        );

Money discountedPrice =
        originalPrice
                .applyDiscount(20);

Now:

originalPrice = BDT 4990.0
discountedPrice = BDT 3992.0

originalPrice change হয়নি।

applyDiscount() নতুন Money object return করেছে।


Avoid Mutation-Looking Method Names

Immutable object-এ:

money.setAmount(...);
money.changeAmount(...);

avoid করা ভালো।

Operations result value return করতে পারে:

money.add(other);
money.applyDiscount(20);

Method name থেকে caller বুঝতে হবে original object unchanged থাকবে কি না।

Java standard library-তে String একই approach ব্যবহার করে।

String original =
        " java ";

String normalized =
        original.strip()
                .toUpperCase();

original unchanged থাকে।


Records as Immutable Data Carriers

Modern Java-তে record concise immutable-style data carrier তৈরি করতে পারে।

Example:

public record CourseCode(
        String value
) {

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

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

Record automatically provide করে:

  • Final components
  • Accessor
  • equals()
  • hashCode()
  • toString()

Usage:

CourseCode code =
        new CourseCode(
                "java-foundation"
        );

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

তবে records:

  • সব domain entities-এর replacement নয়
  • Components-এর referenced objects deeply immutable করে না
  • Mutable collection component থাকলে mutation leak হতে পারে

এই course-এ normal classes প্রথমে শেখানো হয়েছে, যাতে underlying concepts clear থাকে।


Engineering Note: Unmodifiable Is Not Always Immutable

An unmodifiable collection caller-কে add বা remove করতে দেয় না।

কিন্তু যদি এটি mutable elements contain করে, elements change হতে পারে।

List<Lesson> lessons =
        List.copyOf(
                sourceLessons
        );

List structure immutable-style।

But:

lessons.get(0)
        .changeTitle(
                "Updated"
        );

possible, যদি Lesson mutable।

Therefore:

Unmodifiable collection

এবং:

Deeply immutable object graph

এক জিনিস নয়।


Common Mistakes

Assuming final Means Immutable

private final List<String> values;

List still mutable হতে পারে।


Keeping Setters in an Immutable Class

public void setValue(
        String value
) {
    this.value =
            value;
}

এটি immutability ভেঙে দেয়।


Returning Mutable Internal State

public List<String> getLessons() {
    return lessons;
}

Caller internal state modify করতে পারে।


Storing Mutable Constructor Arguments Directly

this.lessons =
        lessons;

Caller original list mutate করলে object change হয়।


Using Mutable Equality Fields

Immutable value object-এর equality stable values-এর ওপর based হওয়া উচিত।


Making Every Entity Immutable Without Need

Naturally changing workflows unnecessarily complex হয়ে যেতে পারে।


Returning this After Mutation and Calling It Immutable

public Money add(
        Money other
) {
    amount +=
            other.amount;

    return this;
}

Object mutate হচ্ছে।

Method new object return করলেও implementation immutable নয়, যদি existing state change হয়।


Practice Exercises

Exercise 1: Create an Immutable EmailAddress

Requirements:

Value required
Surrounding spaces remove
Lowercase normalize
Must contain @
No setter
Value-based equals and hashCode

Exercise 2: Identify Mutation Leaks

Review:

public final class CoursePlan {

    private final List<String> lessons;

    public CoursePlan(
            List<String> lessons
    ) {
        this.lessons =
                lessons;
    }

    public List<String> getLessons() {
        return lessons;
    }
}

Identify every way external code can modify the plan।


Exercise 3: Apply Defensive Copying

Refactor CoursePlan using:

List.copyOf(...)

Reject:

  • Null list
  • Null lesson titles
  • Blank lesson titles

Exercise 4: Entity or Value Object

Classify:

Learner
CourseCode
Enrollment
Money
EmailAddress
Course

Explain which should likely be immutable।


Exercise 5: Immutable Operation

Implement:

Money subtract(
        Money other
)

Rules:

  • other required
  • Result cannot be negative
  • Original objects unchanged
  • Return a new Money

Exercise 6: Find the False Immutability

public final class Schedule {

    private final Date startDate;

    public Date getStartDate() {
        return startDate;
    }
}

Explain why mutable Date can break immutability।

Suggest defensive copying or a modern immutable date type such as LocalDateTime


Exercise 7: Choose Controlled Mutability

For course enrollment progress, compare:

  1. Mutating the existing Enrollment
  2. Returning a new Enrollment after every completed lesson

Discuss simplicity, auditability, and allocation trade-offs।


Predict the Result

Question 1

final List<String> lessons =
        new ArrayList<>();

lessons.add(
        "Introduction"
);

Will it compile?


Question 2

final List<String> lessons =
        new ArrayList<>();

lessons =
        new ArrayList<>();

Will reassignment compile?


Question 3

Money original =
        new Money(
                100_000L
        );

Money discounted =
        original.applyDiscount(
                20
        );

What is the original amount after the call?


Question 4

If CourseCode has no setters and all fields are private final immutable values, can caller change its value after construction?


Predict the Result Answers

Answer 1

হ্যাঁ।

final reference same list point করছে; list-এর content mutate করা যায়।

Answer 2

না।

Final reference reassign করা যায় না।

Answer 3

100000 paisa

Method নতুন object return করে; original unchanged।

Answer 4

না, normal public API দিয়ে change করা যাবে না।


Knowledge Check

Question 1

Immutable object কী?

Question 2

final field কী prevent করে?

Question 3

final reference কি nested object mutation prevent করে?

Question 4

Defensive copy কী?

Question 5

Mutable constructor argument direct store করা risky কেন?

Question 6

Mutable internal collection getter দিয়ে return করা risky কেন?

Question 7

Shallow এবং deep immutability-এর difference কী?

Question 8

Value objects immutability-এর ভালো candidate কেন?

Question 9

Immutable object concurrency-তে useful কেন?

Question 10

সব domain entity immutable হওয়া উচিত কি?

Question 11

Immutable operation existing object change না করে কী করে?

Question 12

Record কি referenced mutable object-কে deeply immutable করে?


Knowledge Check Answers

Answer 1

যে object creation-এর পরে observable state পরিবর্তন করতে দেয় না।

Answer 2

Field value বা reference reassign করা।

Answer 3

না।

Answer 4

External mutable value-এর independent copy তৈরি করে internal state হিসেবে রাখা বা return করা।

Answer 5

Caller original object mutate করলে immutable class-এর internal stateও change হতে পারে।

Answer 6

Caller controlled methods bypass করে internal state change করতে পারে।

Answer 7

Shallow immutability own fields protect করে। Deep immutability nested object graph-এর mutationও prevent করে।

Answer 8

তাদের identity value দ্বারা determined এবং value change হলে নতুন value represent করা natural।

Answer 9

State change না হওয়ায় concurrent readers-এর মধ্যে mutation race থাকে না।

Answer 10

না। Naturally stateful entities controlled mutable হতে পারে।

Answer 11

নতুন object বা value return করে।

Answer 12

না। Record components mutable object reference করলে nested state mutate হতে পারে।


Lesson Summary

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

  • Mutable object-এর state creation-এর পরে change হয়
  • Immutable object observable state change করতে দেয় না
  • Fields private এবং final রাখা immutability-এর foundation
  • Constructor complete valid state initialize করে
  • Immutable class setters expose করে না
  • final reference nested object mutation prevent করে না
  • Mutable inputs direct store করলে mutation leak হয়
  • Defensive copying internal state protect করে
  • Mutable internal values direct return করা উচিত নয়
  • Unmodifiable collection deeply immutable নাও হতে পারে
  • Nested elementsও immutable হলে deep immutability সহজ হয়
  • String immutable হওয়ায় safely share করা যায়
  • Value objects immutability-এর strong candidates
  • Immutable equality এবং hash code stable থাকে
  • Immutable objects reasoning, testing, sharing এবং concurrency সহজ করে
  • Immutable operation original object mutate না করে নতুন object return করে
  • Entities এবং value objects-এর mutability requirements আলাদা
  • Controlled mutability naturally stateful workflows-এর জন্য appropriate
  • final class subclass-based mutation contract prevent করতে সাহায্য করে
  • Records concise immutable-style data carrier provide করে
  • Records deep immutability automatically guarantee করে না
  • Immutability একটি design tool, universal rule নয়

Next Lesson

পরবর্তী lesson:

Packages and Organizing Java Code

আমরা শিখব:

  • Package কী
  • Package declaration
  • Import statement
  • Class file organization
  • Package naming conventions
  • Package-private access
  • Organizing code by feature
  • Avoiding large utility and model packages
  • Basic project structure for a maintainable Java application