Java Good Class Design: Packages, Access Control & SOLID Principles

โ˜• Java 21+ LTS ๐ŸŸข Chapter 46 of 47 ๐Ÿ“‚ Phase 10: Encapsulation & Access Modifiers ๐Ÿ“… 2026 Edition
๐Ÿ“Œ Covered in this chapter:

Java Package System ยท Package Declaration & import ยท Package-Private vs Public API Design ยท Naming Conventions ยท Single Responsibility Principle ยท Cohesion vs Coupling ยท Encapsulation Violation Patterns (Anti-Patterns) ยท Access Modifier Decision Flowchart

Mastering professional class design in Java: the package system for organizing and isolating code, package-private boundary enforcement, access modifier decision frameworks, coupling vs cohesion trade-offs, and recognizing common encapsulation anti-patterns that create fragile, hard-to-maintain software.

1. The Java Package System

A Package is a namespace mechanism that groups related classes and interfaces together, serving two purposes:
1. Organization: Hierarchically categorizes classes (like folders on a filesystem).
2. Access Control: The default (package-private) modifier creates an internal API boundary visible only within the package.

Package Naming Convention (Reverse Domain):

package com.ourcompiler.java.tutorial.phase10;
// com = top-level domain (reversed)
// ourcompiler = company name
// java.tutorial.phase10 = product and feature hierarchy

Declaring and Importing:

// In Animal.java:
package com.zoo.animals;
public class Animal { ... }

// In Main.java (different package):
package com.zoo.management;
import com.zoo.animals.Animal; // Import specific class
import com.zoo.animals.*; // Import all classes in package (Discouraged in production)

2. Package-Private: Building Internal API Boundaries

Using default (package-private) access strategically creates clean internal API boundaries:

// Package: com.company.payments

// INTERNAL implementation (package-private: only used inside the package)
class EncryptionUtil {
static String encrypt(String data) { ... }
}

class FraudDetector {
boolean isSuspicious(double amount) { ... }
}

// PUBLIC API (the only surface exposed to external packages)
public class PaymentProcessor {
public boolean processPayment(String card, double amount) {
String encrypted = EncryptionUtil.encrypt(card); // OK: same package
if (new FraudDetector().isSuspicious(amount)) return false; // OK: same package
// ... actual processing
return true;
}
}
// External packages ONLY see PaymentProcessor; EncryptionUtil and FraudDetector are hidden!

3. Good Class Design Principles

1. Single Responsibility Principle (SRP):
A class should have exactly one reason to change. Avoid God Classes that do everything:

Bad:  class UserManager { register() + sendEmail() + saveToDatabase() + generateReport() }
Good: class UserRegistrar, class EmailSender, class UserRepository, class ReportGenerator

2. High Cohesion โ€” Related Concepts Together:
All methods and fields in a class should relate to a single, well-defined concept. If a class has fields for both "employee details" and "payroll calculation rules", consider splitting them.

3. Low Coupling โ€” Minimize Dependencies:
A class should know as little as possible about other classes. Prefer working with abstract interfaces rather than concrete implementations.

4. Encapsulation as a First Principle:
Always default to private. Only expose a member when you have a specific, justified reason to do so.

4. The Access Modifier Decision Flowchart

When choosing an access modifier for a field or method, ask these questions in order:

Q1: Is this part of the PUBLIC API used by external packages?
       YES โ†’ public
       NO  โ†’ Q2

Q2: Is this needed by SUBCLASSES in other packages?
YES โ†’ protected
NO โ†’ Q3

Q3: Is this shared among MULTIPLE classes in the SAME package?
YES โ†’ default (no modifier)
NO โ†’ private

The Default Answer is ALWAYS: private!

5. Encapsulation Anti-Patterns to Avoid

Anti-Pattern 1: Public Fields (God Mode Access)

// TERRIBLE!
class User { public String password; } // Anyone can read/write password!

Anti-Pattern 2: Getter-Setter for Everything (Anemic Domain Model)

// Technically encapsulated but semantically broken:
account.setBalance(account.getBalance() - withdrawAmount); // Logic leaked outside!
// Better: account.withdraw(amount) โ€” keeps logic inside the class!

Anti-Pattern 3: Returning Internal Mutable Collections

public List<String> getTags() { return tags; } // External code can add/remove tags!
// Better:
public List<String> getTags() { return Collections.unmodifiableList(tags); }

Beginner Example & Code Anatomy

โ˜• Main.java โ€” Chapter 46 Core Example
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

// ---------------------------------------------------------------
// WELL-DESIGNED ENCAPSULATED CLASS: CourseEnrollment System
// ---------------------------------------------------------------
class Course {
    private final String courseId;
    private final String title;
    private final int maxCapacity;
    private final List<String> enrolledStudentIds; // Mutable internally

    public Course(String courseId, String title, int maxCapacity) {
        if (maxCapacity <= 0) throw new IllegalArgumentException("Capacity must be positive.");
        this.courseId          = courseId;
        this.title             = title;
        this.maxCapacity       = maxCapacity;
        this.enrolledStudentIds = new ArrayList<>();
    }

    // Read-only getters for immutable fields
    public String getCourseId()  { return courseId; }
    public String getTitle()     { return title; }
    public int getMaxCapacity()  { return maxCapacity; }

    // Computed properties
    public int getEnrolledCount()    { return enrolledStudentIds.size(); }
    public boolean isFullyBooked()   { return enrolledStudentIds.size() >= maxCapacity; }
    public int getAvailableSeats()   { return maxCapacity - enrolledStudentIds.size(); }

    // Return UNMODIFIABLE view โ€” prevents external mutation!
    public List<String> getEnrolledStudents() {
        return Collections.unmodifiableList(enrolledStudentIds);
    }

    // Business operation (logic stays inside the class)
    public boolean enroll(String studentId) {
        if (studentId == null || studentId.isBlank()) return false;
        if (isFullyBooked()) {
            System.out.println("  [ENROLL FAIL] Course '" + title + "' is fully booked!");
            return false;
        }
        if (enrolledStudentIds.contains(studentId)) {
            System.out.println("  [ENROLL FAIL] Student " + studentId + " already enrolled.");
            return false;
        }
        enrolledStudentIds.add(studentId);
        System.out.printf("  [ENROLL OK] %s enrolled in '%s'. Seats remaining: %d%n",
                studentId, title, getAvailableSeats());
        return true;
    }

    public boolean unenroll(String studentId) {
        boolean removed = enrolledStudentIds.remove(studentId);
        if (removed) System.out.printf("  [UNENROLL] %s removed from '%s'.%n", studentId, title);
        return removed;
    }

    @Override
    public String toString() {
        return String.format("Course{id='%s', title='%s', seats=%d/%d, booked=%b}",
                courseId, title, getEnrolledCount(), maxCapacity, isFullyBooked());
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println("=== Course Enrollment System ===");
        Course javaAdvanced = new Course("CS-401", "Advanced Java Programming", 3);

        System.out.println("Initial State: " + javaAdvanced);

        System.out.println("
--- Enrollment Phase ---");
        javaAdvanced.enroll("STU-001");
        javaAdvanced.enroll("STU-002");
        javaAdvanced.enroll("STU-001"); // Duplicate!
        javaAdvanced.enroll("STU-003"); // Last seat!
        javaAdvanced.enroll("STU-004"); // Should FAIL โ€” fully booked

        System.out.println("
Full State: " + javaAdvanced);

        System.out.println("
--- Unmodifiable List Protection ---");
        List<String> students = javaAdvanced.getEnrolledStudents();
        System.out.println("Enrolled students: " + students);
        try {
            students.add("STU-HACKER"); // Attempt external mutation!
        } catch (UnsupportedOperationException e) {
            System.out.println("External mutation BLOCKED! Unmodifiable list protected internal state.");
        }

        System.out.println("
--- Unenrollment ---");
        javaAdvanced.unenroll("STU-002");
        System.out.println("After unenroll: " + javaAdvanced);

        System.out.println("
--- Good Design Check ---");
        System.out.printf("Available seats: %d | Is full: %b%n",
                javaAdvanced.getAvailableSeats(), javaAdvanced.isFullyBooked());
    }
}
๐Ÿ’ป Program Console Output
=== Course Enrollment System === Initial State: Course{id='CS-401', title='Advanced Java Programming', seats=0/3, booked=false} --- Enrollment Phase --- [ENROLL OK] STU-001 enrolled in 'Advanced Java Programming'. Seats remaining: 2 [ENROLL OK] STU-002 enrolled in 'Advanced Java Programming'. Seats remaining: 1 [ENROLL FAIL] Student STU-001 already enrolled. [ENROLL OK] STU-003 enrolled in 'Advanced Java Programming'. Seats remaining: 0 [ENROLL FAIL] Course 'Advanced Java Programming' is fully booked! Full State: Course{id='CS-401', title='Advanced Java Programming', seats=3/3, booked=true} --- Unmodifiable List Protection --- Enrolled students: [STU-001, STU-002, STU-003] External mutation BLOCKED! Unmodifiable list protected internal state. --- Unenrollment --- [UNENROLL] STU-002 removed from 'Advanced Java Programming'. After unenroll: Course{id='CS-401', title='Advanced Java Programming', seats=2/3, booked=false} --- Good Design Check --- Available seats: 1 | Is full: false

๐Ÿ” Line-by-Line Code Explanation

Collections.unmodifiableList(enrolledStudentIds)

Wraps the internal list in an immutable view โ€” callers can read but cannot add, remove, or clear elements.

public boolean enroll(String studentId)

Business logic lives INSIDE the class (not in caller code), keeping rules cohesive and centralized.

public boolean isFullyBooked()

Computed boolean property derived from comparing list size and capacity โ€” no backing field needed.

private final List<String> enrolledStudentIds

Private final reference prevents reassigning the list variable, but internal add/remove operations still work.

Practical Real-World Example

โ˜• PracticalApplication.java โ€” Industry Implementation
// Industry Simulation: Configuration Management Module
// Internal helper (package-private โ€” hidden from external packages)
class ConfigValidator {
    static void validateKey(String key) {
        if (key == null || !key.matches("[A-Z_]+"))
            throw new IllegalArgumentException("Invalid config key: " + key);
    }
}

// Public API class
public class PracticalApplication {
    // Simulated config store
    static java.util.Map<String, String> config = new java.util.HashMap<>();

    public static void setConfig(String key, String value) {
        ConfigValidator.validateKey(key); // Internal utility โ€” same package
        config.put(key, value);
    }

    public static String getConfig(String key) {
        return config.getOrDefault(key, "NOT_SET");
    }

    public static void main(String[] args) {
        setConfig("DB_HOST", "localhost");
        setConfig("DB_PORT", "5432");
        System.out.println("DB_HOST: " + getConfig("DB_HOST"));
        System.out.println("DB_PORT: " + getConfig("DB_PORT"));
        System.out.println("DB_PASS: " + getConfig("DB_PASS")); // Not set
    }
}
๐Ÿ’ป Practical Console Output
DB_HOST: localhost DB_PORT: 5432 DB_PASS: NOT_SET
โš ๏ธ Common Mistakes & Professional Best Practices
  • Returning a mutable List or Map directly from a getter โ€” use Collections.unmodifiableList() or return a copy.
  • Using wildcard imports (import com.company.*;) in production code โ€” always import specific classes for clarity.
  • Putting all classes in the default (unnamed) package โ€” this prevents using the default access modifier effectively.
  • Designing "Anemic Domain Models" where classes have only getters/setters and all logic lives in external "Service" classes.
๐ŸŽฏ Hands-on Coding Challenge

Test your understanding by writing the code directly in your editor or running in our online Java compiler:

โ˜• Challenge.java
// Coding Challenge:
// Create a Playlist class:
// 1. private final String playlistName (read-only).
// 2. private List<String> songs (mutable internally, unmodifiable externally).
// 3. Public methods: addSong(String song), removeSong(String song), getSongs(), contains(String).
// 4. Return an unmodifiable view from getSongs().

import java.util.*;

class Playlist {
    private final String playlistName;
    private final List<String> songs = new ArrayList<>();

    public Playlist(String name) {
        if (name == null || name.isBlank()) throw new IllegalArgumentException("Name required.");
        this.playlistName = name.trim();
    }

    public String getPlaylistName() { return playlistName; }
    public List<String> getSongs()  { return Collections.unmodifiableList(songs); }
    public boolean contains(String song) { return songs.contains(song); }

    public void addSong(String song) {
        if (song != null && !song.isBlank() && !contains(song)) {
            songs.add(song.trim());
        }
    }

    public void removeSong(String song) { songs.remove(song); }
}

public class Challenge {
    public static void main(String[] args) {
        Playlist p = new Playlist("Coding Vibes");
        p.addSong("Lo-Fi Hip Hop"); p.addSong("Chill Beats"); p.addSong("Focus Zone");
        System.out.println("Playlist: " + p.getSongs());
    }
}

๐Ÿ’ก Frequently Asked Questions & Interview Insights

โ“ What is the unnamed default package in Java?

Classes without a `package` declaration belong to the unnamed default package. While fine for quick experiments, production code should always use named packages for proper access control and modularity.

โ“ What does Collections.unmodifiableList() do?

It wraps an existing List in an unmodifiable view. Any attempt to call `add()`, `remove()`, or `clear()` on the returned view throws `UnsupportedOperationException` at runtime. The underlying list itself is still modifiable via the original reference.

โ“ What is the difference between high cohesion and low coupling?

Cohesion measures how closely related the responsibilities within a single class are (high = good). Coupling measures how dependent a class is on other classes (low = good). Good design achieves both simultaneously.

๐Ÿš€ Quick Chapter Recap

  • Java packages provide hierarchical namespacing and package-level access boundaries.
  • Package-private (default) access creates clean internal API isolation without exposing internals.
  • Always return Collections.unmodifiableList() or copies when exposing mutable collection fields.
  • Business logic should live inside the class (cohesive), not scattered in external caller code.
  • Default access modifier choice: private โ€” widen only when there is a justified architectural need.
โ† Prev: 45. Immutable Objects & final Next: 47. Capstone Projects (4 Secure Systems) โ†’
OC
Curated by Our Compiler Java Technical Editorial Team
Published for 2026 Academic & Enterprise Reference ยท 100% Free & Open Access