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:
java.util.Date vs java.sql.Date) eliminates ambiguity.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.
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.
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 representationSystem – Standard I/O, environment, garbage collectionMath – Mathematical operations (abs, sqrt, pow, random)Object – The root superclass of every Java classInteger, Double, Boolean – Wrapper classes for primitivesThread, Runnable – MultithreadingException, RuntimeException – Exception hierarchyStringBuilder – Efficient mutable string building| 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; ArrayListnames = 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.
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 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 + "}";
}
}
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;
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.
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.
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();
}
}
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.*;
While wildcard imports save typing, they are generally discouraged in professional codebases for several reasons:
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.// 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 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;
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 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. |
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.
| 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
}
}
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).
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.
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.
com.google, org.apache, io.github.username.my-company.com becomes com.my_company.123data.com), prefix it with an underscore: com._123data.switch.io), prefix it with an underscore: io._switch.com.myapp.model rather than com.myapp.models (though both are seen in practice).| 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 |
| 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
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
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 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
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 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
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();
}
}
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();
}
}
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;
}
}
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
Notice how the dependencies flow in one direction:
com.school.app depends on com.school.service and com.school.modelcom.school.service depends on com.school.model and com.school.utilcom.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.
These are mistakes that every Java developer makes at least once. Understanding them saves hours of debugging.
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;
// 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
}
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());
}
}
// 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<>();
}
}
// === 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!
}
}
// 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.
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.
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.
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.
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.
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.
Always use lowercase, always use reverse domain, and never use special characters. Consistency across the team and across the industry makes code universally readable.
| 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 |
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: // // 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
// === 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();
}
}
// === 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();
}
}
// === 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, MapbookQuantities) { 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)
// === 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)
| 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. |