Create your own
Lesson illustration

Organizing Java Classes with Packages, Imports, and Access Modifiers

Hello again. Last lesson introduced classes as objects with state and behavior, and used private fields plus public methods to preserve valid state. That boundary is useful only when a project has more than two files. Backend applications quickly grow into dozens of types, so Java needs a way to name, group, and selectively expose them.

In this lesson, you will place the Subscription example into named packages, import it from another package, and choose among public, private, package-private, and protected access. By the end, you should be able to read a typical Java package declaration and understand why an apparently valid class or method is inaccessible.


Packages are names, organization, and boundaries

A package groups related Java types. It serves three practical purposes:

  1. It makes related code easier to find.
  2. It prevents naming collisions.
  3. It participates in access control.

For example, these are different types because their fully qualified names differ:

com.grasp.subscription.domain.Subscription
com.grasp.billing.domain.Subscription

Both classes may use the simple name Subscription, but their package names identify which one is meant.

If your Unity work included C#, packages will resemble namespaces at first: both organize types and help avoid collisions. Java packages do more, though: package membership also determines whether package-private members are accessible. Java also conventionally aligns package names with folders beneath a source root.

For a small backend-oriented example, use this structure:

src/
  com/
    grasp/
      subscription/
        app/
          SubscriptionDemo.java
        domain/
          Subscription.java
          SubscriptionRules.java

The package names are:

com.grasp.subscription.app
com.grasp.subscription.domain

Each dot in the name corresponds to a folder beneath the source root, here src.

Package names are conventionally:

  • all lowercase;
  • organized from broad to specific;
  • prefixed with a reversed domain name in professional code, such as com.company.project.

Your future Spring Boot application might begin with a package such as com.yourname.taskboard. The exact name matters less than choosing a unique, consistent prefix and keeping related code together.

The following official Dev.java reading establishes the terminology and rules. It is particularly useful for distinguishing a package name from a class name and for seeing the three ways to refer to a type in another package.

Packages - Dev.java

Read “Packages” from Dev.java for the core rules behind package declarations, package naming, imports, and naming conflicts.

In “Understanding Packages” and “Creating a Package,” read the package foundation. Notice that the package statement applies to every type in its source file. Then read “Naming a Package and Naming Conventions,” focusing on the naming convention. In “Using Package Members,” read the qualified-name rationale, then skim “Name Ambiguities” for the collision rule.

Declaring a package

The first code statement in a packaged Java file is its package declaration:

package com.grasp.subscription.domain;

public class Subscription {
    // class body
}

The declaration is not a comment or a label. It tells the compiler that the fully qualified name of this type is:

com.grasp.subscription.domain.Subscription

A file with no package declaration belongs to the unnamed package, sometimes called the default package. It is acceptable for tiny learning programs, such as the two files from the previous lesson. Do not treat it as normal application structure: classes in named packages cannot import classes from the unnamed package.

One subtle but important point: package names that look hierarchical are still distinct packages.

com.grasp.subscription
com.grasp.subscription.domain

The second is not “inside” the first for access purposes. Being in com.grasp.subscription does not grant package-private access to com.grasp.subscription.domain.


Imports make external types convenient to name

A class can refer directly to types in its own package. A class in a different package must either use a fully qualified name or import the public type it needs.

Here is the Subscription class moved into the domain package.

src/com/grasp/subscription/domain/Subscription.java

package com.grasp.subscription.domain;

public class Subscription {
    private final String planName;
    private final int monthlyRequestLimit;
    private int requestsUsed;

    public Subscription(String planName, int monthlyRequestLimit) {
        if (planName == null || planName.isBlank()) {
            throw new IllegalArgumentException("planName must not be blank");
        }

        if (!SubscriptionRules.hasValidLimit(monthlyRequestLimit)) {
            throw new IllegalArgumentException(
                "monthlyRequestLimit must be positive"
            );
        }

        this.planName = planName;
        this.monthlyRequestLimit = monthlyRequestLimit;
        this.requestsUsed = 0;
    }

    public String getPlanName() {
        return planName;
    }

    public int getRequestsRemaining() {
        return monthlyRequestLimit - requestsUsed;
    }

    public boolean tryConsumeRequest() {
        if (requestsUsed >= monthlyRequestLimit) {
            return false;
        }

        requestsUsed++;
        return true;
    }
}

The demo is in a different package, so it imports Subscription.

src/com/grasp/subscription/app/SubscriptionDemo.java

package com.grasp.subscription.app;

import com.grasp.subscription.domain.Subscription;

public class SubscriptionDemo {

    public static void main(String[] args) {
        Subscription starterPlan = new Subscription("Starter", 3);

        boolean accepted = starterPlan.tryConsumeRequest();

        System.out.println("Request accepted: " + accepted);
        System.out.println(
            starterPlan.getPlanName()
                + " requests remaining: "
                + starterPlan.getRequestsRemaining()
        );
    }
}

The standard file order is:

  1. package declaration, if there is one;
  2. import declarations;
  3. class, interface, enum, or record declaration.

An import does not move, copy, instantiate, or make a type public. It is simply permission to write the simple name Subscription instead of the full name com.grasp.subscription.domain.Subscription.

These two declarations mean the same thing:

import com.grasp.subscription.domain.Subscription;

Subscription plan = new Subscription("Starter", 3);
com.grasp.subscription.domain.Subscription plan =
    new com.grasp.subscription.domain.Subscription("Starter", 3);

Imports are normally clearer for types used repeatedly. Prefer importing individual types:

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

Java also permits a wildcard import:

import java.util.*;

That syntax imports types directly in java.util, not types in its subpackages. It does not import java.util.concurrent, for example. Wildcards do not make the program slower, but specific imports make dependencies clearer and avoid confusion when projects grow.

You do not need to import String, System, or Math because Java automatically makes the java.lang package available. Java also makes the current package available, which is why Subscription can use SubscriptionRules without an import.

This video gives an IDE-based demonstration of creating packages, importing a class, and seeing the difference between public and package-private access.

Public, Default Access Specifiers, Packages | Java Object Oriented Tutorials

Watch “Public, Default Access Specifiers, Packages” by LearningLad to connect the source-code rules to what an IDE shows when a type is accessible or inaccessible.

Watch package imports to see a class become usable from another package. Continue with public access, then skip to package private. Focus on the compiler errors: an import statement and an access modifier solve different problems.

When names collide

Suppose a file needs two classes named Date:

java.util.Date
java.sql.Date

You cannot make the name unambiguous by importing both and then writing only Date. Use a fully qualified name for at least one of them:

import java.util.Date;

public class DateExample {
    private Date createdAt;
    private java.sql.Date databaseDate;
}

The same rule applies to your own code. Packages prevent the global naming collision, but your current file still must state clearly which identically named type it uses.


Access modifiers: who may use this member?

An access modifier controls who may access a type or member. Java has four access levels:

Access levelWritten asTypical purpose
privateprivateInternal state and implementation details of one class
Package-privateno modifierCollaboration within one package
protectedprotectedSame-package access, plus limited inheritance-based access
publicpublicDeliberate API available from other packages

“Package-private” is often casually called “default access,” but there is no default keyword for it. You obtain it by writing no modifier.

Here is a useful visibility map for members:

Code trying to access the memberprivatePackage-privateprotectedpublic
Same classYesYesYesYes
Different class, same packageNoYesYesYes
Subclass in a different packageNoNoYes, through inheritanceYes
Non-subclass in a different packageNoNoNoYes

The final two rows explain why protected must not be treated as “public for subclasses.” Any class in the same package can use a protected member, even if it is not a subclass.

The diagram shows `Alpha` in Package One and its subclass `AlphaSub` in Package Two. It illustrates the cross-package inheritance situation in which Java’s `protected` access can matter.

private: preserve the class invariant

You already used private in Subscription:

private int requestsUsed;

No code outside Subscription can assign to that field directly. This lets the class ensure that usage never becomes negative or exceeds the allowed limit.

Use private by default for fields. Also use it for helper methods that are implementation details rather than operations that another class should invoke.

Package-private: implementation shared within one package

Now add a small rule class beside Subscription.

src/com/grasp/subscription/domain/SubscriptionRules.java

package com.grasp.subscription.domain;

final class SubscriptionRules {

    static boolean hasValidLimit(int monthlyRequestLimit) {
        return monthlyRequestLimit > 0;
    }
}

Notice the absence of an access modifier before both class and static boolean. They are package-private.

Subscription can call this method because both classes are in com.grasp.subscription.domain:

if (!SubscriptionRules.hasValidLimit(monthlyRequestLimit)) {
    throw new IllegalArgumentException(
        "monthlyRequestLimit must be positive"
    );
}

However, this code in SubscriptionDemo, which belongs to com.grasp.subscription.app, does not compile:

import com.grasp.subscription.domain.SubscriptionRules;

// Error: SubscriptionRules is not public and cannot be accessed

Adding an import cannot bypass access control. The import tells Java which type you mean; the modifier determines whether you are allowed to use it.

Package-private is useful when several closely related classes implement an internal feature but should not become part of the package’s public API. It is a more focused boundary than public, but it is not the same as C#’s internal, which is based on an assembly. Java package-private access is based on the exact package name.

public: the API other packages may depend on

The demo in com.grasp.subscription.app can create a Subscription because both the class and its constructor are public:

public class Subscription {
    public Subscription(String planName, int monthlyRequestLimit) {
        // ...
    }
}

This two-level check matters:

  • A public method inside a package-private class is still unavailable outside the package.
  • A public class with a package-private constructor cannot be constructed outside its package.

For top-level types such as ordinary classes, Java permits only:

public class Subscription {
}

or package-private:

class SubscriptionRules {
}

A top-level class cannot itself be declared private or protected. Those modifiers are valid for members nested inside another class, a topic that can wait until it becomes useful.

protected: an inheritance extension point

protected is primarily for a superclass that intentionally allows subclasses to access or customize part of its implementation. In a different package, a subclass can access the protected member through inheritance, typically with this or its own inherited member.

There is one Java-specific subtlety worth remembering: a subclass in another package cannot use protected as permission to access that member on an arbitrary superclass object. This keeps the permission tied to the subclass relationship.

For ordinary backend domain classes, protected is usually less common than private, package-private, and public. Do not choose it “just in case.” Make a member protected only when inheritance is a conscious part of the design. You will evaluate inheritance versus composition in the next module.


Compile the packaged example from the command line

IDEs create folders and imports for you, but compiling once from the command line makes the package rules concrete.

From the src directory containing the com folder, compile all three files into a separate output directory:

javac -d ../out com/grasp/subscription/domain/SubscriptionRules.java com/grasp/subscription/domain/Subscription.java com/grasp/subscription/app/SubscriptionDemo.java

Then run the fully qualified name of the class containing main:

java -cp ../out com.grasp.subscription.app.SubscriptionDemo

The important details are:

  • -d ../out tells javac where to write compiled .class files while preserving their package folders.
  • -cp ../out tells java where the package root of compiled classes is.
  • com.grasp.subscription.app.SubscriptionDemo is a fully qualified class name. Do not add .java or .class.

Common package-related errors are usually structural rather than mysterious:

Error or symptomLikely causeCheck
package ... does not existSource root, package declaration, and folder path disagreeStart compilation from the source root and verify each folder matches the package name
cannot find symbol for SubscriptionMissing import or misspelled type/package nameAdd a specific import, or use the fully qualified name temporarily
X is not public ... cannot be accessed from outside packageCode is trying to use a package-private type or member externallyMove the caller, expose a deliberate public operation, or keep the boundary
Could not find or load main classRunning with a simple class name, wrong classpath, or wrong output folderUse the fully qualified main-class name and correct -cp path

In IntelliJ IDEA, create packages under the marked src source root rather than manually making folders in arbitrary places. The IDE will insert the package declaration and maintain the matching directory structure. Still read the declaration at the top of each file; it remains the authoritative statement of a class’s package.


Key takeaways

Packages group related types, prevent naming conflicts, and form an access boundary. A class’s package comes from its package declaration, and its folder path should match that package beneath the source root.

Use specific imports to refer conveniently to public types in other packages. Imports do not grant access, import subpackages, or copy code into a file. When names collide, use a fully qualified name to state precisely which type you mean.

For access choices, use private for internal mutable state, package-private for internal cooperation inside a package, public for intentional external API, and protected only for deliberate inheritance extension points. Remember that a type’s accessibility and its member’s accessibility are checked separately.

Next, you will trace Java object references and add null-safety checks, which will help you diagnose one of the most common runtime failures in Java: NullPointerException.

Can't find a good explanation? Sign up and we'll make it for you

Sign up