Packages




1. What is a Package?

A package in Java is a way of grouping related classes, interfaces, and sub-packages together. It serves the same purpose as a folder on your computer: it organizes files so you can find them, prevents naming conflicts, and controls who can access what.

Real-world analogy: Imagine a large hospital. The hospital has departments – Cardiology, Neurology, Emergency, Radiology. Each department has its own staff, equipment, and procedures. A nurse named “Sarah” might work in Cardiology, and another nurse named “Sarah” might work in Neurology. There is no confusion because the department acts as a namespace. You say “Sarah from Cardiology” or “Sarah from Neurology.” Java packages work the same way. You can have a User class in com.myapp.model and another User class in com.thirdparty.auth without any conflict.

Packages solve three fundamental problems:

  • Organization – As projects grow from 10 classes to 10,000 classes, packages keep the codebase navigable. Without them, you would have thousands of files in a single directory.
  • Namespace management – Two classes can have the same name as long as they live in different packages. The fully qualified name (java.util.Date vs java.sql.Date) eliminates ambiguity.
  • Access control – Java’s default (package-private) access modifier restricts visibility to classes within the same package. This lets you hide implementation details from the rest of the application.

Package Naming Convention

Java uses a reverse domain name convention for package names. If your company’s domain is example.com, your packages start with com.example. This guarantees global uniqueness – no two organizations will accidentally create the same package name.

// Reverse domain naming examples:
// Company domain: google.com       -> com.google.gson, com.google.common.collect
// Company domain: apache.org       -> org.apache.commons.lang3
// Company domain: springframework  -> org.springframework.boot
// Personal project: myapp          -> com.myapp.model, com.myapp.service

// A class inside a package
package com.myapp.model;

public class User {
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() { return name; }
    public String getEmail() { return email; }
}

The package statement tells the compiler: “This class belongs to com.myapp.model.” Every other class in the project can now refer to it by its fully qualified name: com.myapp.model.User.



2. Built-in Java Packages

Java ships with hundreds of packages in the standard library (the Java API). You do not need to download or install anything extra – they are available the moment you install the JDK. Understanding the most common packages will make you significantly more productive.

java.lang – The Foundation (Auto-Imported)

The java.lang package is so fundamental that Java imports it automatically into every class. You never write import java.lang.String; – it is already there. This package contains the building blocks of every Java program:

  • String – Text representation
  • System – Standard I/O, environment, garbage collection
  • Math – Mathematical operations (abs, sqrt, pow, random)
  • Object – The root superclass of every Java class
  • Integer, Double, Boolean – Wrapper classes for primitives
  • Thread, Runnable – Multithreading
  • Exception, RuntimeException – Exception hierarchy
  • StringBuilder – Efficient mutable string building

Overview of Key Packages

Package Purpose Key Classes
java.lang Core language classes (auto-imported) String, Math, System, Object, Integer, Thread, StringBuilder
java.util Collections, utilities, date/time ArrayList, HashMap, HashSet, Scanner, Optional, Collections
java.io Input/output, file handling File, InputStream, OutputStream, BufferedReader, PrintWriter
java.nio Non-blocking I/O, modern file API Path, Files, Paths, ByteBuffer, Channel
java.net Networking URL, HttpURLConnection, Socket, ServerSocket, URI
java.sql Database access (JDBC) Connection, Statement, ResultSet, DriverManager, PreparedStatement
java.time Modern date/time (Java 8+) LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Duration
java.util.stream Functional stream operations Stream, Collectors, IntStream, DoubleStream
java.util.concurrent Concurrency utilities ExecutorService, Future, CompletableFuture, ConcurrentHashMap
java.util.function Functional interfaces Function, Predicate, Consumer, Supplier, BiFunction
// Examples of using built-in packages

// java.lang -- no import needed
String greeting = "Hello, Java!";
int absolute = Math.abs(-42);           // 42
double squareRoot = Math.sqrt(144);     // 12.0
System.out.println(greeting);

// java.util -- must import
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Scanner;

ArrayList names = new ArrayList<>();
names.add("Alice");
names.add("Bob");

HashMap scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 87);

Scanner scanner = new Scanner(System.in);

// java.io -- must import
import java.io.File;
import java.io.BufferedReader;
import java.io.FileReader;

File file = new File("data.txt");
boolean exists = file.exists();

// java.time -- must import (Java 8+)
import java.time.LocalDate;
import java.time.LocalDateTime;

LocalDate today = LocalDate.now();
LocalDateTime now = LocalDateTime.now();

A key takeaway: every class in Java belongs to a package. When you write a class without a package statement, it goes into the default package (unnamed package). This is acceptable for quick experiments but should never be used in production code because classes in the default package cannot be imported by classes in named packages.



3. Creating Packages

Creating your own package involves two steps: declaring the package in your source file and organizing the file into the correct directory structure. These two must match exactly – if they do not, the compiler will refuse to compile your code.

The package Statement

The package statement must be the very first line of your Java source file (before any imports, class declarations, or comments, with the exception of regular comments). There can be only one package statement per file.

// File: com/myapp/models/User.java

package com.myapp.models;  // MUST be the first statement

import java.time.LocalDate;  // imports come after the package statement

public class User {
    private String name;
    private String email;
    private LocalDate createdAt;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
        this.createdAt = LocalDate.now();
    }

    public String getName() { return name; }
    public String getEmail() { return email; }
    public LocalDate getCreatedAt() { return createdAt; }

    @Override
    public String toString() {
        return "User{name='" + name + "', email='" + email + "', createdAt=" + createdAt + "}";
    }
}

Directory Structure Must Match

This is the most critical rule about packages: the directory structure on disk must exactly mirror the package declaration. Each dot in the package name represents a directory separator.

If your package is com.myapp.models, the file must be located at:

// Package: com.myapp.models
// Directory structure:

project-root/
  src/
    com/
      myapp/
        models/
          User.java        // package com.myapp.models;
          Product.java     // package com.myapp.models;
        services/
          UserService.java    // package com.myapp.services;
          ProductService.java // package com.myapp.services;
        utils/
          Validator.java   // package com.myapp.utils;
        app/
          Main.java        // package com.myapp.app;

Compiling and Running with Packages

When you compile and run classes that belong to packages, you need to be aware of the source root. The compiler and JVM use the package name to locate files.

// Compiling from the project root (src/ directory):

// Step 1: Compile the User class
// Command: javac com/myapp/models/User.java

// Step 2: Compile Main that uses User
// Command: javac com/myapp/app/Main.java

// Step 3: Run the Main class using its fully qualified name
// Command: java com.myapp.app.Main
// NOTE: Use dots (not slashes) and do NOT include .class extension

// Compile all files at once:
// Command: javac com/myapp/models/*.java com/myapp/services/*.java com/myapp/app/Main.java

// Or compile everything recursively (Java 9+):
// Command: javac $(find . -name "*.java")

Note: Modern IDEs like IntelliJ IDEA and Eclipse handle directory creation and compilation automatically. When you declare a package in a new file, the IDE creates the matching folder structure for you. However, understanding the underlying mechanism is essential for debugging classpath issues and working with build tools like Maven and Gradle.



4. Importing Packages

When you want to use a class from another package, you have two options: use the fully qualified name every time, or import it once and use the short name everywhere. Import statements appear after the package declaration and before the class declaration.

Single Class Import

The most common and recommended approach is importing the exact class you need:

package com.myapp.app;

// Single class imports -- explicit and clear
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import com.myapp.models.User;
import com.myapp.services.UserService;

public class Main {
    public static void main(String[] args) {
        // No need to write java.util.ArrayList -- just ArrayList
        ArrayList users = new ArrayList<>();
        Map userMap = new HashMap<>();

        UserService service = new UserService();
    }
}

Wildcard Import

The wildcard * imports all classes from a package. It does not import sub-packages – only classes directly in that package.

package com.myapp.app;

// Wildcard import -- imports ALL classes from java.util
import java.util.*;
import com.myapp.models.*;

public class Main {
    public static void main(String[] args) {
        // All of these work because we imported java.util.*
        ArrayList list = new ArrayList<>();
        HashMap map = new HashMap<>();
        HashSet set = new HashSet<>();
        Scanner scanner = new Scanner(System.in);

        // This works because we imported com.myapp.models.*
        User user = new User("Alice", "alice@example.com");
    }
}

// IMPORTANT: Wildcard does NOT import sub-packages!
import java.util.*;    // imports ArrayList, HashMap, etc.
                       // does NOT import java.util.stream.Stream
                       // does NOT import java.util.concurrent.ExecutorService

// You would need separate imports for sub-packages:
import java.util.stream.*;
import java.util.concurrent.*;

Why Wildcard Imports Are Discouraged in Production

While wildcard imports save typing, they are generally discouraged in professional codebases for several reasons:

  • Ambiguity – If you import java.util.* and java.sql.*, and then use Date, the compiler cannot tell whether you mean java.util.Date or java.sql.Date. You get a compilation error.
  • Readability – Explicit imports tell the reader exactly which classes are used. A new developer can glance at the imports and immediately understand the dependencies.
  • Future conflicts – A new version of a library might add a class whose name conflicts with one of your other imports.
// Ambiguity problem with wildcard imports:
import java.util.*;  // contains java.util.Date
import java.sql.*;   // contains java.sql.Date

public class DateProblem {
    public static void main(String[] args) {
        // COMPILE ERROR: reference to Date is ambiguous
        // Date date = new Date();

        // Solution 1: Use fully qualified name
        java.util.Date utilDate = new java.util.Date();
        java.sql.Date sqlDate = new java.sql.Date(System.currentTimeMillis());

        // Solution 2: Import one explicitly, qualify the other
        // At top of file: import java.util.Date;
        // Then: Date utilDate = new Date();
        // And:  java.sql.Date sqlDate = new java.sql.Date(...);
    }
}

Static Imports

Static imports let you use static methods and constants from another class without qualifying them with the class name. This is useful for utility methods you call frequently, but overuse can make code confusing.

// Without static import
public class CircleCalc {
    public double area(double radius) {
        return Math.PI * Math.pow(radius, 2);
    }

    public double hypotenuse(double a, double b) {
        return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
    }
}

// With static import -- cleaner for math-heavy code
import static java.lang.Math.PI;
import static java.lang.Math.pow;
import static java.lang.Math.sqrt;

public class CircleCalc {
    public double area(double radius) {
        return PI * pow(radius, 2);
    }

    public double hypotenuse(double a, double b) {
        return sqrt(pow(a, 2) + pow(b, 2));
    }
}

// Static wildcard import -- imports ALL static members
import static java.lang.Math.*;

public class MathDemo {
    public static void main(String[] args) {
        System.out.println("PI = " + PI);                    // 3.141592653589793
        System.out.println("sqrt(16) = " + sqrt(16));        // 4.0
        System.out.println("abs(-7) = " + abs(-7));          // 7
        System.out.println("max(10, 20) = " + max(10, 20));  // 20
        System.out.println("random() = " + random());        // e.g., 0.7234...
    }
}

// Common static imports in testing (JUnit 5)
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

Fully Qualified Name (No Import)

You can always skip the import and use the full package + class name. This is common when you have a naming conflict or use a class only once:

public class FullyQualifiedExample {
    public static void main(String[] args) {
        // Using fully qualified names -- no import statement needed
        java.util.ArrayList names = new java.util.ArrayList<>();
        names.add("Alice");

        java.time.LocalDate today = java.time.LocalDate.now();
        System.out.println("Today: " + today);

        // This is verbose, but sometimes necessary to resolve conflicts
        java.util.Date utilDate = new java.util.Date();
        java.sql.Date sqlDate = new java.sql.Date(utilDate.getTime());
    }
}

Import Summary

Import Style Syntax Use When
Single class import java.util.ArrayList; Default choice. Explicit and clear.
Wildcard import java.util.*; Quick prototyping. Avoid in production code.
Static single import static java.lang.Math.PI; Frequently used constants or assertions.
Static wildcard import static java.lang.Math.*; Math-heavy code. Use sparingly.
Fully qualified java.util.Date d = new java.util.Date(); Resolving name conflicts. One-time use.



5. Access Control with Packages

Packages are deeply connected to Java’s access control system. The default access modifier (also called package-private) relies entirely on packages to determine visibility. When you declare a field, method, or class without any access modifier, it is visible only to other classes in the same package.

This is an intentional design decision. It lets you create helper classes and internal APIs that are visible within a module (package) but hidden from the rest of the application. Think of it as the “internal affairs” of a department – other departments do not see your internal memos.

Visibility Table

Modifier Same Class Same Package Subclass (Different Package) Any Class (Different Package)
public Yes Yes Yes Yes
protected Yes Yes Yes No
default (no modifier) Yes Yes No No
private Yes No No No
// === File: com/myapp/models/User.java ===
package com.myapp.models;

public class User {
    public String name;             // visible everywhere
    protected String email;         // visible in same package + subclasses
    String nickname;                // package-private: visible ONLY in com.myapp.models
    private String passwordHash;    // visible ONLY within this User class

    public User(String name, String email, String nickname, String passwordHash) {
        this.name = name;
        this.email = email;
        this.nickname = nickname;
        this.passwordHash = passwordHash;
    }

    // Package-private method -- only classes in com.myapp.models can call this
    String getPasswordHash() {
        return passwordHash;
    }

    // Public method -- anyone can call this
    public String getDisplayName() {
        return name + " (" + nickname + ")";
    }
}

// === File: com/myapp/models/UserValidator.java ===
package com.myapp.models;  // SAME package

public class UserValidator {
    public boolean isValid(User user) {
        // CAN access public field
        System.out.println(user.name);           // OK

        // CAN access protected field (same package)
        System.out.println(user.email);          // OK

        // CAN access package-private field (same package)
        System.out.println(user.nickname);       // OK

        // CANNOT access private field
        // System.out.println(user.passwordHash); // COMPILE ERROR

        // CAN access package-private method (same package)
        String hash = user.getPasswordHash();    // OK
        return hash != null && !hash.isEmpty();
    }
}

// === File: com/myapp/services/UserService.java ===
package com.myapp.services;  // DIFFERENT package

import com.myapp.models.User;

public class UserService {
    public void processUser(User user) {
        // CAN access public field
        System.out.println(user.name);           // OK

        // CANNOT access protected field (different package, not a subclass)
        // System.out.println(user.email);       // COMPILE ERROR

        // CANNOT access package-private field (different package)
        // System.out.println(user.nickname);    // COMPILE ERROR

        // CANNOT access private field
        // System.out.println(user.passwordHash); // COMPILE ERROR

        // CANNOT access package-private method (different package)
        // user.getPasswordHash();               // COMPILE ERROR

        // CAN access public method
        System.out.println(user.getDisplayName()); // OK
    }
}

Protected Access and Inheritance Across Packages

The protected modifier has a nuanced behavior. Within the same package, it behaves like package-private (any class can access it). Across packages, only subclasses can access it, and only through inheritance (not through an instance reference).

// === File: com/myapp/models/User.java ===
package com.myapp.models;

public class User {
    protected String email;

    public User(String email) {
        this.email = email;
    }
}

// === File: com/myapp/services/AdminUser.java ===
package com.myapp.services;  // different package

import com.myapp.models.User;

public class AdminUser extends User {

    public AdminUser(String email) {
        super(email);
    }

    public void showEmail() {
        // CAN access protected field through inheritance
        System.out.println(this.email);  // OK -- accessing through 'this'
    }

    public void showOtherEmail(User other) {
        // CANNOT access protected field through an instance of the parent class
        // System.out.println(other.email);  // COMPILE ERROR
    }
}

// Output:
// The protected field 'email' is accessible via this.email (inheritance),
// but NOT via other.email (instance reference from a different package).

Package-Private Classes

Even entire classes can be package-private. This is common for internal helper classes that should not be used outside the package:

// === File: com/myapp/models/PasswordEncoder.java ===
package com.myapp.models;

// No 'public' keyword -- this class is package-private
class PasswordEncoder {
    static String encode(String rawPassword) {
        // Simple hash for demonstration (use BCrypt in production!)
        return Integer.toHexString(rawPassword.hashCode());
    }

    static boolean matches(String rawPassword, String encodedPassword) {
        return encode(rawPassword).equals(encodedPassword);
    }
}

// === File: com/myapp/models/User.java ===
package com.myapp.models;

public class User {
    private String name;
    private String passwordHash;

    public User(String name, String rawPassword) {
        this.name = name;
        // Can use PasswordEncoder because it is in the same package
        this.passwordHash = PasswordEncoder.encode(rawPassword);
    }

    public boolean checkPassword(String rawPassword) {
        return PasswordEncoder.matches(rawPassword, this.passwordHash);
    }
}

// === File: com/myapp/services/UserService.java ===
package com.myapp.services;

import com.myapp.models.User;
// import com.myapp.models.PasswordEncoder; // COMPILE ERROR -- not visible!

public class UserService {
    public void createUser(String name, String password) {
        User user = new User(name, password);  // OK -- User is public
        // PasswordEncoder.encode(password);   // COMPILE ERROR -- not accessible
    }
}

This pattern is powerful. The User class uses PasswordEncoder internally, but no class outside the com.myapp.models package can even see that PasswordEncoder exists. The implementation detail is completely hidden.



6. Package Naming Conventions

Java has well-established conventions for naming packages. Following them is not optional in professional projects – it is expected. Consistent naming makes code predictable and avoids collisions with third-party libraries.

Rules

  • All lowercase – Package names are always written in lowercase letters. No camelCase, no UPPER_CASE.
  • Reverse domain name – Start with your organization’s reversed internet domain name: com.google, org.apache, io.github.username.
  • No hyphens – Use underscores if your domain contains hyphens: my-company.com becomes com.my_company.
  • No starting with digits – If a domain component starts with a digit (e.g., 123data.com), prefix it with an underscore: com._123data.
  • No Java keywords – If a domain component is a Java keyword (e.g., switch.io), prefix it with an underscore: io._switch.
  • Singular nouns preferred – Use com.myapp.model rather than com.myapp.models (though both are seen in practice).

Common Package Structure Patterns

Pattern Example Description
By layer com.myapp.controller, com.myapp.service, com.myapp.repository Organizes by architectural layer (most common in Spring Boot)
By feature com.myapp.user, com.myapp.order, com.myapp.payment Groups all classes for a feature together
Hybrid com.myapp.user.controller, com.myapp.user.service Feature first, then layer within each feature

Real-World Examples

Project Package Examples
Spring Framework org.springframework.boot, org.springframework.web.bind.annotation, org.springframework.data.jpa.repository
Google Guava com.google.common.collect, com.google.common.base, com.google.common.io
Apache Commons org.apache.commons.lang3, org.apache.commons.io, org.apache.commons.collections4
JUnit 5 org.junit.jupiter.api, org.junit.jupiter.params
Jackson JSON com.fasterxml.jackson.databind, com.fasterxml.jackson.annotation
// GOOD -- follows conventions
package com.lovemesomecoding.tutorial;
package com.myapp.user.service;
package org.opensource.util;
package io.github.myusername.calculator;

// BAD -- violates conventions
package Com.MyApp.Models;     // uppercase letters
package my-app.models;        // hyphens not allowed
package 123data.processor;    // starts with digit
package com.myapp.Class;      // uppercase, and 'Class' is ambiguous
package stuff;                // vague name, no domain prefix



7. Sub-packages

A sub-package is a package that lives inside another package in the directory hierarchy. For example, java.util.stream is a sub-package of java.util, and java.util.concurrent.locks is a sub-package of java.util.concurrent.

However, there is a crucial concept to understand: sub-packages have NO special relationship with their parent package in terms of access control. A sub-package is treated as a completely independent package. Classes in java.util cannot see package-private members of classes in java.util.stream, and vice versa.

// Sub-package directory structure
com/
  myapp/
    model/                  // package com.myapp.model
      User.java
      Order.java
    model/
      dto/                  // package com.myapp.model.dto  (sub-package of model)
        UserDTO.java
        OrderDTO.java
    service/                // package com.myapp.service
      UserService.java
    service/
      impl/                 // package com.myapp.service.impl  (sub-package of service)
        UserServiceImpl.java

Sub-packages Do NOT Inherit Access

This is a common misconception. Many beginners assume that because com.myapp.model.dto is “inside” com.myapp.model, it inherits the package-private access of com.myapp.model. It does not. They are separate packages.

// === File: com/myapp/model/User.java ===
package com.myapp.model;

public class User {
    String internalId;  // package-private -- only visible in com.myapp.model

    public String name;

    public User(String name) {
        this.internalId = "USR-" + System.nanoTime();
        this.name = name;
    }
}

// === File: com/myapp/model/UserRepository.java ===
package com.myapp.model;  // SAME package as User

public class UserRepository {
    public void save(User user) {
        // CAN access package-private field -- same package
        System.out.println("Saving user with internal ID: " + user.internalId);  // OK
    }
}

// === File: com/myapp/model/dto/UserDTO.java ===
package com.myapp.model.dto;  // SUB-PACKAGE -- treated as a DIFFERENT package

import com.myapp.model.User;

public class UserDTO {
    public String name;

    public static UserDTO fromUser(User user) {
        UserDTO dto = new UserDTO();
        dto.name = user.name;            // OK -- public field

        // CANNOT access package-private field from sub-package!
        // dto.id = user.internalId;     // COMPILE ERROR

        return dto;
    }
}

Importing Sub-packages

Importing a parent package does not automatically import its sub-packages. Each must be imported individually:

// Importing parent does NOT import sub-packages
import java.util.*;          // imports ArrayList, HashMap, etc.
                              // does NOT import Stream, Collectors, etc.

// You must import sub-packages separately
import java.util.stream.*;           // Stream, Collectors
import java.util.concurrent.*;       // ExecutorService, Future
import java.util.concurrent.locks.*; // Lock, ReentrantLock

// Similarly for your own packages:
import com.myapp.model.*;       // imports User, Order
                                 // does NOT import UserDTO from com.myapp.model.dto
import com.myapp.model.dto.*;   // imports UserDTO, OrderDTO



8. Creating a Multi-Package Project

Now let us build a complete multi-package project from scratch. This is a Student Management System with four packages, each with a clear responsibility. This example demonstrates how classes interact across packages, when to use public vs. package-private, and how imports work in practice.

Project Structure

// Project directory structure:
//
// src/
//   com/
//     school/
//       model/
//         Student.java       -- data class
//         Course.java        -- data class
//         Enrollment.java    -- package-private helper
//       service/
//         StudentService.java  -- business logic
//         CourseService.java   -- business logic
//       util/
//         Validator.java     -- validation utilities
//         IdGenerator.java   -- package-private utility
//       app/
//         Main.java          -- entry point

Package 1: com.school.model

The model package contains the data classes. Notice that Enrollment is package-private – it is an implementation detail of the model package.

// === File: com/school/model/Student.java ===
package com.school.model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Student {
    private final String id;
    private String name;
    private String email;
    private final List enrollments;  // uses package-private Enrollment

    public Student(String id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.enrollments = new ArrayList<>();
    }

    public String getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }

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

    // Package-private -- only model classes can call this
    void addEnrollment(Enrollment enrollment) {
        this.enrollments.add(enrollment);
    }

    // Public -- returns an unmodifiable view of course names
    public List getEnrolledCourseNames() {
        List names = new ArrayList<>();
        for (Enrollment e : enrollments) {
            names.add(e.getCourseName());
        }
        return Collections.unmodifiableList(names);
    }

    public int getEnrollmentCount() {
        return enrollments.size();
    }

    @Override
    public String toString() {
        return "Student{id='" + id + "', name='" + name + "', courses=" + getEnrolledCourseNames() + "}";
    }
}
// === File: com/school/model/Course.java ===
package com.school.model;

public class Course {
    private final String code;
    private String title;
    private int maxCapacity;
    private int currentEnrollment;

    public Course(String code, String title, int maxCapacity) {
        this.code = code;
        this.title = title;
        this.maxCapacity = maxCapacity;
        this.currentEnrollment = 0;
    }

    public String getCode() { return code; }
    public String getTitle() { return title; }
    public int getMaxCapacity() { return maxCapacity; }
    public int getCurrentEnrollment() { return currentEnrollment; }

    public boolean hasAvailableSeats() {
        return currentEnrollment < maxCapacity;
    }

    // Package-private -- only model classes can increment enrollment
    void incrementEnrollment() {
        currentEnrollment++;
    }

    @Override
    public String toString() {
        return "Course{code='" + code + "', title='" + title +
               "', enrolled=" + currentEnrollment + "/" + maxCapacity + "}";
    }
}
// === File: com/school/model/Enrollment.java ===
package com.school.model;

import java.time.LocalDate;

// Package-private class -- NOT visible outside com.school.model
// This is an implementation detail. Other packages interact with
// Student and Course directly, never with Enrollment.
class Enrollment {
    private final String studentId;
    private final String courseCode;
    private final String courseName;
    private final LocalDate enrollDate;

    Enrollment(String studentId, String courseCode, String courseName) {
        this.studentId = studentId;
        this.courseCode = courseCode;
        this.courseName = courseName;
        this.enrollDate = LocalDate.now();
    }

    String getStudentId() { return studentId; }
    String getCourseCode() { return courseCode; }
    String getCourseName() { return courseName; }
    LocalDate getEnrollDate() { return enrollDate; }

    // Static factory method used by Student and Course within this package
    static void enroll(Student student, Course course) {
        if (!course.hasAvailableSeats()) {
            throw new IllegalStateException("Course " + course.getCode() + " is full");
        }
        Enrollment enrollment = new Enrollment(student.getId(), course.getCode(), course.getTitle());
        student.addEnrollment(enrollment);
        course.incrementEnrollment();
    }
}

Package 2: com.school.util

The utility package contains shared helper classes. IdGenerator is package-private because only Validator uses it internally.

// === File: com/school/util/Validator.java ===
package com.school.util;

public class Validator {

    public static boolean isValidEmail(String email) {
        if (email == null || email.isBlank()) return false;
        return email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$");
    }

    public static boolean isValidName(String name) {
        if (name == null || name.isBlank()) return false;
        return name.length() >= 2 && name.length() <= 100;
    }

    public static boolean isValidCourseCode(String code) {
        if (code == null || code.isBlank()) return false;
        // Course codes must be like "CS101", "MATH200"
        return code.matches("^[A-Z]{2,5}\\d{3}$");
    }

    public static String generateStudentId() {
        return IdGenerator.generate("STU");
    }

    public static String generateCourseId() {
        return IdGenerator.generate("CRS");
    }
}

// === File: com/school/util/IdGenerator.java ===
package com.school.util;

import java.util.concurrent.atomic.AtomicLong;

// Package-private -- only Validator exposes the ID generation functionality
class IdGenerator {
    private static final AtomicLong counter = new AtomicLong(1000);

    static String generate(String prefix) {
        return prefix + "-" + counter.incrementAndGet();
    }
}

Package 3: com.school.service

The service package contains business logic. It depends on the model and util packages but not the other way around. This is a clean, one-way dependency.

// === File: com/school/service/StudentService.java ===
package com.school.service;

import com.school.model.Student;
import com.school.model.Course;
import com.school.util.Validator;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class StudentService {
    private final Map students = new HashMap<>();

    public Student createStudent(String name, String email) {
        // Use Validator from the util package
        if (!Validator.isValidName(name)) {
            throw new IllegalArgumentException("Invalid name: " + name);
        }
        if (!Validator.isValidEmail(email)) {
            throw new IllegalArgumentException("Invalid email: " + email);
        }

        String id = Validator.generateStudentId();
        Student student = new Student(id, name, email);
        students.put(id, student);
        return student;
    }

    public Student findById(String id) {
        Student student = students.get(id);
        if (student == null) {
            throw new IllegalArgumentException("Student not found: " + id);
        }
        return student;
    }

    public List findAll() {
        return new ArrayList<>(students.values());
    }

    // Notice: we cannot access Enrollment directly because it is package-private
    // in com.school.model. We cannot do "new Enrollment(...)" here.
    // We must use public methods on Student and Course instead.
}
// === File: com/school/service/CourseService.java ===
package com.school.service;

import com.school.model.Course;
import com.school.model.Student;
import com.school.util.Validator;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class CourseService {
    private final Map courses = new HashMap<>();

    public Course createCourse(String code, String title, int capacity) {
        if (!Validator.isValidCourseCode(code)) {
            throw new IllegalArgumentException("Invalid course code: " + code +
                ". Must be 2-5 uppercase letters followed by 3 digits (e.g., CS101).");
        }
        if (courses.containsKey(code)) {
            throw new IllegalArgumentException("Course already exists: " + code);
        }

        Course course = new Course(code, title, capacity);
        courses.put(code, course);
        return course;
    }

    public Course findByCode(String code) {
        Course course = courses.get(code);
        if (course == null) {
            throw new IllegalArgumentException("Course not found: " + code);
        }
        return course;
    }

    public List findAll() {
        return new ArrayList<>(courses.values());
    }

    public List findAvailable() {
        List available = new ArrayList<>();
        for (Course course : courses.values()) {
            if (course.hasAvailableSeats()) {
                available.add(course);
            }
        }
        return available;
    }
}

Package 4: com.school.app

The application package contains the entry point. It depends on service and model packages. It orchestrates the entire application.

// === File: com/school/app/Main.java ===
package com.school.app;

import com.school.model.Student;
import com.school.model.Course;
import com.school.service.StudentService;
import com.school.service.CourseService;
// import com.school.model.Enrollment;    // COMPILE ERROR -- Enrollment is package-private!
// import com.school.util.IdGenerator;    // COMPILE ERROR -- IdGenerator is package-private!

public class Main {
    public static void main(String[] args) {
        StudentService studentService = new StudentService();
        CourseService courseService = new CourseService();

        // Create courses
        Course java = courseService.createCourse("CS101", "Introduction to Java", 30);
        Course python = courseService.createCourse("CS102", "Python Programming", 25);
        Course database = courseService.createCourse("CS201", "Database Design", 20);

        System.out.println("--- Courses Created ---");
        for (Course course : courseService.findAll()) {
            System.out.println("  " + course);
        }

        // Create students
        Student alice = studentService.createStudent("Alice Johnson", "alice@university.edu");
        Student bob = studentService.createStudent("Bob Smith", "bob@university.edu");

        System.out.println("\n--- Students Created ---");
        for (Student student : studentService.findAll()) {
            System.out.println("  " + student);
        }

        // Show available courses
        System.out.println("\n--- Available Courses ---");
        for (Course course : courseService.findAvailable()) {
            System.out.println("  " + course.getCode() + ": " + course.getTitle() +
                " (" + (course.getMaxCapacity() - course.getCurrentEnrollment()) + " seats left)");
        }

        System.out.println("\n--- Package Visibility Demo ---");
        System.out.println("We can access Student.getName(): " + alice.getName());
        System.out.println("We can access Course.getTitle(): " + java.getTitle());
        System.out.println("We CANNOT access Enrollment -- it is package-private in com.school.model");
        System.out.println("We CANNOT access IdGenerator -- it is package-private in com.school.util");
    }
}

// Output:
// --- Courses Created ---
//   Course{code='CS101', title='Introduction to Java', enrolled=0/30}
//   Course{code='CS102', title='Python Programming', enrolled=0/25}
//   Course{code='CS201', title='Database Design', enrolled=0/20}
//
// --- Students Created ---
//   Student{id='STU-1001', name='Alice Johnson', courses=[]}
//   Student{id='STU-1002', name='Bob Smith', courses=[]}
//
// --- Available Courses ---
//   CS101: Introduction to Java (30 seats left)
//   CS102: Python Programming (25 seats left)
//   CS201: Database Design (20 seats left)
//
// --- Package Visibility Demo ---
//   We can access Student.getName(): Alice Johnson
//   We can access Course.getTitle(): Introduction to Java
//   We CANNOT access Enrollment -- it is package-private in com.school.model
//   We CANNOT access IdGenerator -- it is package-private in com.school.util

Dependency Flow

Notice how the dependencies flow in one direction:

  • com.school.app depends on com.school.service and com.school.model
  • com.school.service depends on com.school.model and com.school.util
  • com.school.model depends on nothing (it is self-contained)
  • com.school.util depends on nothing (it is self-contained)

This is the hallmark of a well-designed package structure: dependencies flow from higher-level packages (app, service) to lower-level packages (model, util), never in reverse. The model and util packages know nothing about services or the application layer.



9. Common Mistakes

These are mistakes that every Java developer makes at least once. Understanding them saves hours of debugging.

Mistake 1: Package Name Does Not Match Directory Structure

The package declaration and the directory path must be identical. If they are not, the compiler will reject the file or you will get ClassNotFoundException at runtime.

// File is located at: src/com/myapp/model/User.java

// WRONG -- package does not match directory
package com.myapp.models;  // says "models" but directory is "model"
// Compiler error: package com.myapp.models does not match directory structure

// CORRECT
package com.myapp.model;  // matches the directory exactly

// WRONG -- using wrong separator
package com/myapp/model;   // slashes instead of dots -- syntax error
package com.myapp.Model;   // capital M does not match lowercase directory

// CORRECT
package com.myapp.model;

Mistake 2: Forgetting the Package Statement

// File: src/com/myapp/model/User.java

// WRONG -- missing package statement
// The file is in com/myapp/model/ but has no package declaration
public class User {
    // This class is in the DEFAULT (unnamed) package
    // Other packaged classes CANNOT import it!
}

// CORRECT
package com.myapp.model;

public class User {
    // Now properly belongs to com.myapp.model
}

Mistake 3: Circular Dependencies

Circular dependencies occur when package A depends on package B and package B depends on package A. While Java does not prevent this, it creates tightly coupled, hard-to-maintain code.

// BAD -- Circular dependency between service and model

// === File: com/myapp/model/User.java ===
package com.myapp.model;
import com.myapp.service.UserService;  // model depends on service

public class User {
    private String name;

    public void save() {
        UserService service = new UserService();
        service.save(this);  // model calling service -- BAD design
    }
}

// === File: com/myapp/service/UserService.java ===
package com.myapp.service;
import com.myapp.model.User;  // service depends on model

public class UserService {
    public void save(User user) {
        System.out.println("Saving " + user);
    }
}

// The dependency is circular: model -> service -> model
// This is a design smell. The model should NOT know about the service.

// GOOD -- One-way dependency: service depends on model, model depends on nothing

// === File: com/myapp/model/User.java ===
package com.myapp.model;

public class User {
    private String name;
    public User(String name) { this.name = name; }
    public String getName() { return name; }
}

// === File: com/myapp/service/UserService.java ===
package com.myapp.service;
import com.myapp.model.User;  // one-way dependency

public class UserService {
    public void save(User user) {
        System.out.println("Saving " + user.getName());
    }
}

Mistake 4: Wildcard Import Conflicts

// Problem: Both packages have a class named "List"
import java.util.*;   // contains java.util.List
import java.awt.*;    // contains java.awt.List

public class ImportConflict {
    public static void main(String[] args) {
        // COMPILE ERROR: reference to List is ambiguous
        // List myList = new ArrayList();

        // Fix: Use explicit import for the one you want
        // Or use fully qualified name:
        java.util.List myList = new java.util.ArrayList<>();
    }
}

Mistake 5: Assuming Sub-package Access

// === File: com/myapp/model/User.java ===
package com.myapp.model;

public class User {
    String internalCode = "ABC";  // package-private
}

// === File: com/myapp/model/dto/UserDTO.java ===
package com.myapp.model.dto;  // this is a DIFFERENT package

import com.myapp.model.User;

public class UserDTO {
    public static void main(String[] args) {
        User user = new User();

        // COMPILE ERROR: internalCode is not visible
        // Many developers expect this to work because dto is "inside" model
        // System.out.println(user.internalCode);

        // Sub-packages are completely separate packages in Java!
    }
}

Mistake 6: Using the Default Package in Production

// File: User.java (no package declaration -- sits in default package)

// No package statement -- this class is in the unnamed "default" package
public class User {
    private String name;
}

// Problem 1: Classes in named packages CANNOT import from the default package
// === File: com/myapp/service/UserService.java ===
package com.myapp.service;

// import User;  // COMPILE ERROR -- cannot import from default package

// Problem 2: Default package classes cannot be referenced by fully qualified name
// because they have no package name

// Rule: ALWAYS use a package statement in every class. The default package
// should only be used for throwaway experiments or learning exercises.



10. Best Practices

Here are the practices that experienced Java developers follow for package organization. These are not just rules from a textbook – they come from years of maintaining large-scale applications.

1. Prefer Feature-Based Over Layer-Based Organization

As projects grow, organizing by feature is usually more maintainable than organizing by layer. With feature-based packages, all related classes are together, making it easy to find, modify, and delete features without hunting across multiple package trees.

// LAYER-BASED (common but can become unwieldy at scale)
com.myapp.controller/
    UserController.java
    OrderController.java
    ProductController.java
com.myapp.service/
    UserService.java
    OrderService.java
    ProductService.java
com.myapp.repository/
    UserRepository.java
    OrderRepository.java
    ProductRepository.java

// Adding a new feature (e.g., "Notification") requires changes to 3+ packages.
// Deleting a feature means removing files from multiple packages.

// FEATURE-BASED (better for larger projects)
com.myapp.user/
    UserController.java
    UserService.java
    UserRepository.java
    User.java
com.myapp.order/
    OrderController.java
    OrderService.java
    OrderRepository.java
    Order.java
com.myapp.product/
    ProductController.java
    ProductService.java
    ProductRepository.java
    Product.java

// Adding or removing a feature = adding or removing one package.
// Everything related to "user" is in one place.

2. Use Package-Private as the Default

Make everything package-private by default, and only promote to public when another package actually needs it. This minimizes your public API surface and gives you freedom to change internal implementation without breaking other packages.

3. Avoid Circular Dependencies

If package A imports from package B and package B imports from package A, you have a circular dependency. Resolve it by introducing a third package for shared types, or by using interfaces to break the cycle.

4. Keep Package Count Manageable

Having 50 packages with 1-2 classes each is just as bad as having 1 package with 200 classes. Aim for a sweet spot where each package contains 5-15 related classes. If a package grows larger than 20 classes, consider splitting it. If it has fewer than 3 classes, consider merging it with a related package.

5. Follow the Naming Conventions Strictly

Always use lowercase, always use reverse domain, and never use special characters. Consistency across the team and across the industry makes code universally readable.

6. Summary Table

Practice Do Do Not
Package naming com.mycompany.project.feature MyPackage, stuff, misc
Default access Make classes/methods package-private by default Make everything public "just in case"
Import style Single class imports: import java.util.ArrayList; Wildcard imports in production: import java.util.*;
Dependencies One-way: app -> service -> model Circular: model -> service -> model
Package size 5-15 closely related classes per package 1 class per package or 100+ classes per package
Organization Feature-based for large projects, layer-based for small Random grouping with no logical structure
Default package Always declare a package Leave classes in the unnamed default package
Sub-packages Use when logical hierarchy exists Assume sub-packages inherit parent access



11. Complete Practical Example: Online Bookstore

Let us build a complete, multi-package Online Bookstore application that demonstrates every concept covered in this tutorial. This example has four packages with deliberate access control decisions, proper imports, and clean one-way dependencies.

Project Structure

// Project structure:
//
// src/
//   com/
//     bookstore/
//       model/
//         Book.java          -- public data class
//         Customer.java      -- public data class
//         Order.java         -- public data class
//         OrderItem.java     -- package-private (internal to model)
//         PriceCalculator.java  -- package-private (internal to model)
//       service/
//         BookService.java     -- public service
//         OrderService.java    -- public service
//       util/
//         Formatter.java     -- public utility
//       app/
//         BookstoreApp.java  -- main entry point

com.bookstore.model - Data Classes

// === File: com/bookstore/model/Book.java ===
package com.bookstore.model;

public class Book {
    private final String isbn;
    private String title;
    private String author;
    private double price;
    private int stock;

    public Book(String isbn, String title, String author, double price, int stock) {
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.price = price;
        this.stock = stock;
    }

    public String getIsbn() { return isbn; }
    public String getTitle() { return title; }
    public String getAuthor() { return author; }
    public double getPrice() { return price; }
    public int getStock() { return stock; }

    // Package-private -- only model classes can modify stock
    void decreaseStock(int quantity) {
        if (quantity > stock) {
            throw new IllegalStateException("Insufficient stock for '" + title +
                "'. Available: " + stock + ", Requested: " + quantity);
        }
        this.stock -= quantity;
    }

    public boolean isInStock() {
        return stock > 0;
    }

    @Override
    public String toString() {
        return "Book{isbn='" + isbn + "', title='" + title + "', author='" + author +
               "', price=$" + String.format("%.2f", price) + ", stock=" + stock + "}";
    }
}
// === File: com/bookstore/model/Customer.java ===
package com.bookstore.model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Customer {
    private final String id;
    private String name;
    private String email;
    private final List orderHistory;

    public Customer(String id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.orderHistory = new ArrayList<>();
    }

    public String getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }

    public List getOrderHistory() {
        return Collections.unmodifiableList(orderHistory);
    }

    // Package-private -- only model classes (Order) can add to history
    void addOrder(Order order) {
        orderHistory.add(order);
    }

    public int getTotalOrders() {
        return orderHistory.size();
    }

    @Override
    public String toString() {
        return "Customer{id='" + id + "', name='" + name + "', orders=" + orderHistory.size() + "}";
    }
}
// === File: com/bookstore/model/OrderItem.java ===
package com.bookstore.model;

// Package-private class -- an implementation detail of Order
// The service layer works with Order, never with OrderItem directly
class OrderItem {
    private final Book book;
    private final int quantity;
    private final double unitPrice;

    OrderItem(Book book, int quantity) {
        this.book = book;
        this.quantity = quantity;
        this.unitPrice = book.getPrice();
    }

    Book getBook() { return book; }
    int getQuantity() { return quantity; }
    double getUnitPrice() { return unitPrice; }

    double getSubtotal() {
        return unitPrice * quantity;
    }

    @Override
    public String toString() {
        return quantity + "x " + book.getTitle() + " @ $" + String.format("%.2f", unitPrice);
    }
}
// === File: com/bookstore/model/PriceCalculator.java ===
package com.bookstore.model;

import java.util.List;

// Package-private class -- pricing logic is internal to the model
class PriceCalculator {
    private static final double TAX_RATE = 0.08;           // 8% tax
    private static final double BULK_DISCOUNT_THRESHOLD = 3; // 3+ items = discount
    private static final double BULK_DISCOUNT_RATE = 0.10;   // 10% off

    static double calculateSubtotal(List items) {
        double subtotal = 0;
        for (OrderItem item : items) {
            subtotal += item.getSubtotal();
        }
        return subtotal;
    }

    static double calculateDiscount(List items) {
        int totalQuantity = 0;
        for (OrderItem item : items) {
            totalQuantity += item.getQuantity();
        }

        if (totalQuantity >= BULK_DISCOUNT_THRESHOLD) {
            return calculateSubtotal(items) * BULK_DISCOUNT_RATE;
        }
        return 0;
    }

    static double calculateTax(double amountAfterDiscount) {
        return amountAfterDiscount * TAX_RATE;
    }

    static double calculateTotal(List items) {
        double subtotal = calculateSubtotal(items);
        double discount = calculateDiscount(items);
        double afterDiscount = subtotal - discount;
        double tax = calculateTax(afterDiscount);
        return afterDiscount + tax;
    }
}
// === File: com/bookstore/model/Order.java ===
package com.bookstore.model;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

public class Order {
    private static int orderCounter = 0;

    private final String orderId;
    private final Customer customer;
    private final List items;   // uses package-private OrderItem
    private final LocalDateTime orderDate;
    private final double subtotal;
    private final double discount;
    private final double tax;
    private final double total;

    // Package-private constructor -- only OrderBuilder or service can create orders
    Order(Customer customer, List items) {
        this.orderId = "ORD-" + (++orderCounter);
        this.customer = customer;
        this.items = new ArrayList<>(items);
        this.orderDate = LocalDateTime.now();

        // Use package-private PriceCalculator
        this.subtotal = PriceCalculator.calculateSubtotal(items);
        this.discount = PriceCalculator.calculateDiscount(items);
        double afterDiscount = subtotal - discount;
        this.tax = PriceCalculator.calculateTax(afterDiscount);
        this.total = PriceCalculator.calculateTotal(items);

        // Decrease stock for each book
        for (OrderItem item : items) {
            item.getBook().decreaseStock(item.getQuantity());
        }

        // Add this order to the customer's history
        customer.addOrder(this);
    }

    public String getOrderId() { return orderId; }
    public String getCustomerName() { return customer.getName(); }
    public LocalDateTime getOrderDate() { return orderDate; }
    public double getSubtotal() { return subtotal; }
    public double getDiscount() { return discount; }
    public double getTax() { return tax; }
    public double getTotal() { return total; }
    public int getItemCount() { return items.size(); }

    // Public method that exposes item info without exposing OrderItem class
    public List getItemDescriptions() {
        List descriptions = new ArrayList<>();
        for (OrderItem item : items) {
            descriptions.add(item.toString());
        }
        return descriptions;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Order ").append(orderId).append(" for ").append(customer.getName()).append("\n");
        sb.append("  Date: ").append(orderDate.toLocalDate()).append("\n");
        for (OrderItem item : items) {
            sb.append("  - ").append(item).append(" = $")
              .append(String.format("%.2f", item.getSubtotal())).append("\n");
        }
        sb.append("  Subtotal:  $").append(String.format("%.2f", subtotal)).append("\n");
        if (discount > 0) {
            sb.append("  Discount:  -$").append(String.format("%.2f", discount)).append(" (10% bulk)\n");
        }
        sb.append("  Tax:       $").append(String.format("%.2f", tax)).append("\n");
        sb.append("  Total:     $").append(String.format("%.2f", total));
        return sb.toString();
    }
}

com.bookstore.util - Utility Classes

// === File: com/bookstore/util/Formatter.java ===
package com.bookstore.util;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class Formatter {

    private static final DateTimeFormatter DATE_FORMAT =
        DateTimeFormatter.ofPattern("MMM dd, yyyy");
    private static final DateTimeFormatter DATE_TIME_FORMAT =
        DateTimeFormatter.ofPattern("MMM dd, yyyy hh:mm a");

    public static String formatPrice(double price) {
        return String.format("$%.2f", price);
    }

    public static String formatDate(LocalDateTime dateTime) {
        return dateTime.format(DATE_FORMAT);
    }

    public static String formatDateTime(LocalDateTime dateTime) {
        return dateTime.format(DATE_TIME_FORMAT);
    }

    public static String padRight(String text, int width) {
        return String.format("%-" + width + "s", text);
    }

    public static String padLeft(String text, int width) {
        return String.format("%" + width + "s", text);
    }

    public static String repeat(String str, int times) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < times; i++) {
            sb.append(str);
        }
        return sb.toString();
    }
}

com.bookstore.service - Business Logic

// === File: com/bookstore/service/BookService.java ===
package com.bookstore.service;

import com.bookstore.model.Book;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class BookService {
    private final Map catalog = new HashMap<>();

    public void addBook(String isbn, String title, String author, double price, int stock) {
        if (catalog.containsKey(isbn)) {
            throw new IllegalArgumentException("Book with ISBN " + isbn + " already exists");
        }
        catalog.put(isbn, new Book(isbn, title, author, price, stock));
    }

    public Book findByIsbn(String isbn) {
        Book book = catalog.get(isbn);
        if (book == null) {
            throw new IllegalArgumentException("Book not found: " + isbn);
        }
        return book;
    }

    public List findByAuthor(String author) {
        List result = new ArrayList<>();
        for (Book book : catalog.values()) {
            if (book.getAuthor().equalsIgnoreCase(author)) {
                result.add(book);
            }
        }
        return result;
    }

    public List findInStock() {
        List result = new ArrayList<>();
        for (Book book : catalog.values()) {
            if (book.isInStock()) {
                result.add(book);
            }
        }
        return result;
    }

    public List getAllBooks() {
        return new ArrayList<>(catalog.values());
    }

    // Notice: BookService CANNOT call book.decreaseStock() because
    // that method is package-private in com.bookstore.model.
    // Stock management happens only through Order creation.
}
// === File: com/bookstore/service/OrderService.java ===
package com.bookstore.service;

import com.bookstore.model.Book;
import com.bookstore.model.Customer;
import com.bookstore.model.Order;
// import com.bookstore.model.OrderItem;       // CANNOT import -- package-private
// import com.bookstore.model.PriceCalculator;  // CANNOT import -- package-private

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class OrderService {
    private final List orders = new ArrayList<>();
    private final Map customers = new HashMap<>();

    public Customer createCustomer(String name, String email) {
        String id = "CUST-" + (customers.size() + 1);
        Customer customer = new Customer(id, name, email);
        customers.put(id, customer);
        return customer;
    }

    public Customer findCustomer(String id) {
        Customer customer = customers.get(id);
        if (customer == null) {
            throw new IllegalArgumentException("Customer not found: " + id);
        }
        return customer;
    }

    // The OrderService creates orders through the model's public API.
    // It passes Book objects and quantities to the model layer,
    // which handles OrderItem creation internally.
    public Order placeOrder(Customer customer, Map bookQuantities) {
        if (bookQuantities.isEmpty()) {
            throw new IllegalArgumentException("Order must contain at least one book");
        }

        // Validate stock before creating the order
        for (Map.Entry entry : bookQuantities.entrySet()) {
            Book book = entry.getKey();
            int quantity = entry.getValue();
            if (quantity <= 0) {
                throw new IllegalArgumentException("Quantity must be positive for: " + book.getTitle());
            }
            if (book.getStock() < quantity) {
                throw new IllegalStateException("Insufficient stock for '" + book.getTitle() +
                    "'. Available: " + book.getStock() + ", Requested: " + quantity);
            }
        }

        // Delegate to model layer -- Order constructor handles OrderItem creation,
        // price calculation, stock decrements, and customer history updates
        Order order = createOrder(customer, bookQuantities);
        orders.add(order);
        return order;
    }

    // Package-private helper that builds the order through the model's internal API
    private Order createOrder(Customer customer, Map bookQuantities) {
        // We use reflection or a factory in real apps. For this tutorial,
        // Order has a package-private constructor accessible within its own package.
        // Since OrderService is in a different package, we would typically use
        // a public factory method on Order. Let us add one:

        return Order.create(customer, bookQuantities);
    }

    public List getAllOrders() {
        return new ArrayList<>(orders);
    }
}

Notice that OrderService calls Order.create() – a public factory method. Let us add that to the Order class:

// Add this public factory method to Order.java:
public static Order create(Customer customer, Map bookQuantities) {
    List items = new ArrayList<>();
    for (Map.Entry entry : bookQuantities.entrySet()) {
        items.add(new OrderItem(entry.getKey(), entry.getValue()));
    }
    return new Order(customer, items);
}

// This is the PUBLIC API for creating orders.
// The OrderItem and PriceCalculator classes remain package-private.
// External packages only see: Order.create(customer, bookQuantities)

com.bookstore.app - Entry Point

// === File: com/bookstore/app/BookstoreApp.java ===
package com.bookstore.app;

import com.bookstore.model.Book;
import com.bookstore.model.Customer;
import com.bookstore.model.Order;
import com.bookstore.service.BookService;
import com.bookstore.service.OrderService;
import com.bookstore.util.Formatter;

// These imports would FAIL -- the classes are package-private:
// import com.bookstore.model.OrderItem;        // COMPILE ERROR
// import com.bookstore.model.PriceCalculator;   // COMPILE ERROR

import java.util.HashMap;
import java.util.Map;

public class BookstoreApp {
    public static void main(String[] args) {
        // --- Set up services ---
        BookService bookService = new BookService();
        OrderService orderService = new OrderService();

        // --- Add books to catalog ---
        bookService.addBook("978-0134685991", "Effective Java", "Joshua Bloch", 45.99, 10);
        bookService.addBook("978-0596009205", "Head First Design Patterns", "Eric Freeman", 39.99, 8);
        bookService.addBook("978-0132350884", "Clean Code", "Robert C. Martin", 34.99, 15);
        bookService.addBook("978-0201633610", "Design Patterns", "Gang of Four", 49.99, 5);

        // --- Display catalog ---
        System.out.println("========== BOOKSTORE CATALOG ==========");
        System.out.println(Formatter.padRight("Title", 30)
            + Formatter.padRight("Author", 20)
            + Formatter.padLeft("Price", 10)
            + Formatter.padLeft("Stock", 8));
        System.out.println(Formatter.repeat("-", 68));

        for (Book book : bookService.getAllBooks()) {
            System.out.println(Formatter.padRight(book.getTitle(), 30)
                + Formatter.padRight(book.getAuthor(), 20)
                + Formatter.padLeft(Formatter.formatPrice(book.getPrice()), 10)
                + Formatter.padLeft(String.valueOf(book.getStock()), 8));
        }

        // --- Create customers ---
        Customer alice = orderService.createCustomer("Alice Johnson", "alice@email.com");
        Customer bob = orderService.createCustomer("Bob Williams", "bob@email.com");

        System.out.println("\n========== CUSTOMERS ==========");
        System.out.println(alice);
        System.out.println(bob);

        // --- Place Order 1: Alice buys 2 books (no bulk discount) ---
        Map aliceBooks = new HashMap<>();
        aliceBooks.put(bookService.findByIsbn("978-0134685991"), 1);  // Effective Java
        aliceBooks.put(bookService.findByIsbn("978-0132350884"), 1);  // Clean Code
        Order order1 = orderService.placeOrder(alice, aliceBooks);

        System.out.println("\n========== ORDER 1 ==========");
        System.out.println(order1);

        // --- Place Order 2: Bob buys 4 books (gets 10% bulk discount) ---
        Map bobBooks = new HashMap<>();
        bobBooks.put(bookService.findByIsbn("978-0596009205"), 2);    // Head First x2
        bobBooks.put(bookService.findByIsbn("978-0201633610"), 2);    // Design Patterns x2
        Order order2 = orderService.placeOrder(bob, bobBooks);

        System.out.println("\n========== ORDER 2 (BULK DISCOUNT) ==========");
        System.out.println(order2);

        // --- Check updated stock ---
        System.out.println("\n========== UPDATED STOCK ==========");
        for (Book book : bookService.getAllBooks()) {
            System.out.println("  " + book.getTitle() + ": " + book.getStock() + " remaining");
        }

        // --- Customer order history ---
        System.out.println("\n========== CUSTOMER HISTORY ==========");
        System.out.println(alice.getName() + " has " + alice.getTotalOrders() + " order(s)");
        System.out.println(bob.getName() + " has " + bob.getTotalOrders() + " order(s)");

        // --- What we CANNOT do from this package ---
        System.out.println("\n========== ACCESS CONTROL DEMO ==========");
        System.out.println("From com.bookstore.app, we CAN:");
        System.out.println("  - Access Book, Customer, Order (public classes)");
        System.out.println("  - Call public methods: book.getTitle(), order.getTotal()");
        System.out.println("  - Use Formatter utility methods");
        System.out.println("From com.bookstore.app, we CANNOT:");
        System.out.println("  - Access OrderItem (package-private in model)");
        System.out.println("  - Access PriceCalculator (package-private in model)");
        System.out.println("  - Call book.decreaseStock() (package-private method)");
        System.out.println("  - Call customer.addOrder() (package-private method)");
    }
}

// Output:
// ========== BOOKSTORE CATALOG ==========
// Title                         Author                   Price   Stock
// --------------------------------------------------------------------
// Effective Java                Joshua Bloch             $45.99      10
// Head First Design Patterns    Eric Freeman             $39.99       8
// Clean Code                    Robert C. Martin         $34.99      15
// Design Patterns               Gang of Four             $49.99       5
//
// ========== CUSTOMERS ==========
// Customer{id='CUST-1', name='Alice Johnson', orders=0}
// Customer{id='CUST-2', name='Bob Williams', orders=0}
//
// ========== ORDER 1 ==========
// Order ORD-1 for Alice Johnson
//   Date: 2026-02-28
//   - 1x Effective Java @ $45.99 = $45.99
//   - 1x Clean Code @ $34.99 = $34.99
//   Subtotal:  $80.98
//   Tax:       $6.48
//   Total:     $87.46
//
// ========== ORDER 2 (BULK DISCOUNT) ==========
// Order ORD-2 for Bob Williams
//   Date: 2026-02-28
//   - 2x Head First Design Patterns @ $39.99 = $79.98
//   - 2x Design Patterns @ $49.99 = $99.98
//   Subtotal:  $179.96
//   Discount:  -$18.00 (10% bulk)
//   Tax:       $12.96
//   Total:     $174.92
//
// ========== UPDATED STOCK ==========
//   Effective Java: 9 remaining
//   Head First Design Patterns: 6 remaining
//   Clean Code: 14 remaining
//   Design Patterns: 3 remaining
//
// ========== CUSTOMER HISTORY ==========
// Alice Johnson has 1 order(s)
// Bob Williams has 1 order(s)
//
// ========== ACCESS CONTROL DEMO ==========
// From com.bookstore.app, we CAN:
//   - Access Book, Customer, Order (public classes)
//   - Call public methods: book.getTitle(), order.getTotal()
//   - Use Formatter utility methods
// From com.bookstore.app, we CANNOT:
//   - Access OrderItem (package-private in model)
//   - Access PriceCalculator (package-private in model)
//   - Call book.decreaseStock() (package-private method)
//   - Call customer.addOrder() (package-private method)



12. Quick Reference

Concept Key Point
Package A namespace for grouping related classes. Maps to directory structure.
package statement Must be the first statement in a Java file. Only one per file.
import statement Lets you use classes from other packages by their short name.
Wildcard import * Imports all classes from a package. Does NOT import sub-packages.
Static import Imports static members (methods, constants) for direct use.
java.lang The only auto-imported package. Contains String, Math, System, Object.
Default (package-private) Visible only within the same package. Sub-packages do NOT count.
Naming convention Reverse domain, all lowercase: com.company.project.module
Sub-packages Logically nested but treated as completely separate packages by Java.
Default package Unnamed package for classes without a package statement. Avoid in production.
Feature-based packages Group all classes for a feature together. Preferred for larger projects.
Circular dependencies Package A imports from B and B imports from A. Always avoid.



Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *