Java default (package-private) Access Modifier
Java default (package-private) Access Modifier
When you write a class, method, or field with no access modifier at all, Java gives it default access — also called package-private. No keyword is needed and none should be added. The absence of a modifier is itself the declaration.
Default access is the middle ground between the total privacy of private and the team-wide sharing of protected. It says: this member belongs to the package as a unit. Classes within the same package can see and use it freely. Everything outside the package — regardless of whether it is a subclass, a sibling package, or a completely unrelated class — cannot.
This is the access level that makes packages feel like modules. A package can have a clean, small public surface for external callers, a set of private implementation details hidden within each class, and a layer of default members shared freely among the cooperating classes that make up the package.
The Rule — No Keyword, Package Only
DEFAULT (package-private) — written by omitting any modifier:
class Helper { } ← default class — visible in this package only
int counter; ← default field — visible in this package only
void process() { } ← default method — visible in this package only
Who can access it?
✓ Any class in the SAME package
✗ Subclasses in a DIFFERENT package
✗ Any non-subclass in a different package
✗ Even a direct subclass in a different package
Note: Unlike protected, default does NOT extend access through inheritance.
A subclass in a different package has NO access to default members of its parent.
Scope comparison:
private → this class only
default → this package (same directory group)
protected → this package + subclasses anywhere
public → everywhere
1 — Default Class: Package-Internal Helper
A top-level class without public is package-private. It cannot be imported by classes in other packages. It exists to serve its package siblings — it is not part of the public API.
1// File: com/devstackflow/payment/PaymentRequest.java
2package com.devstackflow.payment;
3
4// public class — visible to all packages (part of the external API)
5public class PaymentRequest {
6
7 private final String merchantId;
8 private final String customerId;
9 private final double amount;
10
11 public PaymentRequest(String merchantId, String customerId, double amount) {
12 this.merchantId = merchantId;
13 this.customerId = customerId;
14 this.amount = amount;
15 }
16
17 public String getMerchantId() { return merchantId; }
18 public String getCustomerId() { return customerId; }
19 public double getAmount() { return amount; }
20
21 @Override
22 public String toString() {
23 return String.format("PaymentRequest[merchant=%s, customer=%s, Rs.%.2f]",
24 merchantId, customerId, amount);
25 }
26}1// File: com/devstackflow/payment/PaymentValidator.java
2package com.devstackflow.payment;
3
4// NO 'public' — package-private class
5// Only classes in com.devstackflow.payment can use this
6class PaymentValidator {
7
8 private static final double MIN_AMOUNT = 1.0;
9 private static final double MAX_AMOUNT = 100_000.0;
10
11 // package-private method — same package only
12 boolean isValid(PaymentRequest request) {
13 if (request == null) return false;
14 if (!isMerchantValid(request.getMerchantId())) return false;
15 if (!isAmountInRange(request.getAmount())) return false;
16 return true;
17 }
18
19 String getValidationError(PaymentRequest request) {
20 if (request == null) return "Request is null.";
21 if (!isMerchantValid(request.getMerchantId())) return "Invalid merchant ID.";
22 if (!isAmountInRange(request.getAmount()))
23 return String.format("Amount must be Rs.%.0f-Rs.%.0f.", MIN_AMOUNT, MAX_AMOUNT);
24 return null;
25 }
26
27 // private helpers — completely internal
28 private boolean isMerchantValid(String merchantId) {
29 return merchantId != null && !merchantId.isBlank()
30 && merchantId.startsWith("MID-");
31 }
32
33 private boolean isAmountInRange(double amount) {
34 return amount >= MIN_AMOUNT && amount <= MAX_AMOUNT;
35 }
36}1// File: com/devstackflow/payment/PaymentLogger.java
2package com.devstackflow.payment;
3
4import java.time.LocalDateTime;
5import java.util.ArrayList;
6import java.util.List;
7
8// package-private — only the payment package uses this
9class PaymentLogger {
10
11 private final List<String> log = new ArrayList<>();
12
13 void record(String status, PaymentRequest request) {
14 String entry = String.format("[%s] %s | %s",
15 LocalDateTime.now().toLocalTime().withNano(0),
16 status, request);
17 log.add(entry);
18 System.out.println(" LOG: " + entry);
19 }
20
21 List<String> getLog() {
22 return List.copyOf(log);
23 }
24}1// File: com/devstackflow/payment/PaymentService.java
2package com.devstackflow.payment;
3
4// public class — this is the external-facing API
5public class PaymentService {
6
7 // Package-private helpers — used freely within the package
8 private final PaymentValidator validator = new PaymentValidator();
9 private final PaymentLogger logger = new PaymentLogger();
10
11 public String process(PaymentRequest request) {
12
13 String error = validator.getValidationError(request); // package-private method
14 if (error != null) {
15 logger.record("REJECTED", request); // package-private method
16 return "REJECTED: " + error;
17 }
18
19 // Simulate processing
20 String txnId = "TXN-" + System.currentTimeMillis();
21 logger.record("APPROVED", request);
22 return "APPROVED: " + txnId;
23 }
24
25 public List<String> getAuditLog() {
26 return logger.getLog(); // package-private method
27 }
28}1// File: com/devstackflow/demo/PaymentDemo.java
2package com.devstackflow.demo;
3
4import com.devstackflow.payment.PaymentRequest;
5import com.devstackflow.payment.PaymentService;
6// import com.devstackflow.payment.PaymentValidator; ← compile error — package-private class
7// import com.devstackflow.payment.PaymentLogger; ← compile error — package-private class
8
9public class PaymentDemo {
10
11 public static void main(String[] args) {
12
13 PaymentService service = new PaymentService(); // public class — OK
14
15 PaymentRequest[] requests = {
16 new PaymentRequest("MID-001", "CUST-501", 2499.0),
17 new PaymentRequest("", "CUST-502", 500.0), // invalid merchant
18 new PaymentRequest("MID-002", "CUST-503", -100.0), // invalid amount
19 new PaymentRequest("MID-003", "CUST-504", 75000.0),
20 };
21
22 System.out.println("╔══════════════════════════════════════════╗");
23 System.out.println("║ PAYMENT SERVICE DEMO ║");
24 System.out.println("╚══════════════════════════════════════════╝\n");
25
26 for (PaymentRequest req : requests) {
27 System.out.println("Request : " + req);
28 System.out.println("Result : " + service.process(req));
29 System.out.println();
30 }
31
32 System.out.println("=== Audit Log ===");
33 service.getAuditLog().forEach(System.out::println);
34
35 // PaymentValidator v = new PaymentValidator(); ← compile error
36 // PaymentLogger l = new PaymentLogger(); ← compile error
37 // service.validator ← compile error — private field
38 }
39}Output:
╔══════════════════════════════════════════╗
║ PAYMENT SERVICE DEMO ║
╚══════════════════════════════════════════╝
Request : PaymentRequest[merchant=MID-001, customer=CUST-501, Rs.2499.00]
LOG: [10:30:00] APPROVED | PaymentRequest[merchant=MID-001, customer=CUST-501, Rs.2499.00]
Result : APPROVED: TXN-1705312200000
Request : PaymentRequest[merchant=, customer=CUST-502, Rs.500.00]
LOG: [10:30:00] REJECTED | PaymentRequest[merchant=, customer=CUST-502, Rs.500.00]
Result : REJECTED: Invalid merchant ID.
Request : PaymentRequest[merchant=MID-002, customer=CUST-503, Rs.-100.00]
LOG: [10:30:00] REJECTED | PaymentRequest[merchant=MID-002, customer=CUST-503, Rs.-100.00]
Result : REJECTED: Amount must be Rs.1-Rs.100000.
Request : PaymentRequest[merchant=MID-003, customer=CUST-504, Rs.75000.00]
LOG: [10:30:00] APPROVED | PaymentRequest[merchant=MID-003, customer=CUST-504, Rs.75000.00]
Result : APPROVED: TXN-1705312200003
=== Audit Log ===
[10:30:00] APPROVED | PaymentRequest[merchant=MID-001, customer=CUST-501, Rs.2499.00]
[10:30:00] REJECTED | PaymentRequest[merchant=, customer=CUST-502, Rs.500.00]
[10:30:00] REJECTED | PaymentRequest[merchant=MID-002, customer=CUST-503, Rs.-100.00]
[10:30:00] APPROVED | PaymentRequest[merchant=MID-003, customer=CUST-504, Rs.75000.00]
PaymentValidator and PaymentLogger are completely invisible outside the package. PaymentService — the only public class — orchestrates them internally. External callers see exactly one class and two methods. The package is a self-contained module.
2 — Default Fields and Methods: Package-Level Sharing
Default fields and methods let classes within a package share data and behaviour without wrapping everything in getters and setters — trusting the package as a controlled, cohesive unit.
1// File: com/devstackflow/cache/CacheEntry.java
2package com.devstackflow.cache;
3
4import java.time.LocalDateTime;
5
6class CacheEntry { // package-private class
7
8 // package-private fields — trusted within the package
9 String key;
10 Object value;
11 LocalDateTime createdAt;
12 LocalDateTime expiresAt;
13 int hitCount;
14
15 CacheEntry(String key, Object value, int ttlSeconds) {
16 this.key = key;
17 this.value = value;
18 this.createdAt = LocalDateTime.now();
19 this.expiresAt = createdAt.plusSeconds(ttlSeconds);
20 this.hitCount = 0;
21 }
22
23 // package-private methods
24 boolean isExpired() {
25 return LocalDateTime.now().isAfter(expiresAt);
26 }
27
28 void recordHit() {
29 hitCount++;
30 }
31
32 @Override
33 public String toString() {
34 return String.format("CacheEntry{key=%s, hits=%d, expired=%s}",
35 key, hitCount, isExpired());
36 }
37}1// File: com/devstackflow/cache/CacheStats.java
2package com.devstackflow.cache;
3
4// package-private — internal statistics tracker
5class CacheStats {
6
7 int totalPuts = 0; // package-private fields
8 int totalHits = 0;
9 int totalMisses = 0;
10 int totalEvictions = 0;
11
12 void recordPut() { totalPuts++; }
13 void recordHit() { totalHits++; }
14 void recordMiss() { totalMisses++; }
15 void recordEviction() { totalEvictions++; }
16
17 double hitRate() {
18 int total = totalHits + totalMisses;
19 return total == 0 ? 0.0 : (totalHits * 100.0) / total;
20 }
21
22 @Override
23 public String toString() {
24 return String.format(
25 "CacheStats{puts=%d, hits=%d, misses=%d, evictions=%d, hitRate=%.1f%%}",
26 totalPuts, totalHits, totalMisses, totalEvictions, hitRate());
27 }
28}1// File: com/devstackflow/cache/LocalCache.java
2package com.devstackflow.cache;
3
4import java.util.HashMap;
5import java.util.Map;
6import java.util.Optional;
7
8public class LocalCache {
9
10 // private — the actual data; not even package siblings touch it directly
11 private final Map<String, CacheEntry> store = new HashMap<>();
12 private final CacheStats stats = new CacheStats();
13 private final int maxSize;
14 private final int defaultTtl;
15
16 public LocalCache(int maxSize, int defaultTtlSeconds) {
17 this.maxSize = maxSize;
18 this.defaultTtl = defaultTtlSeconds;
19 }
20
21 public void put(String key, Object value) {
22 if (store.size() >= maxSize) {
23 evictOldest(); // private
24 }
25 store.put(key, new CacheEntry(key, value, defaultTtl));
26 stats.recordPut(); // package-private field and method
27 }
28
29 public Optional<Object> get(String key) {
30 CacheEntry entry = store.get(key);
31 if (entry == null) {
32 stats.recordMiss();
33 return Optional.empty();
34 }
35 if (entry.isExpired()) { // package-private method
36 store.remove(key);
37 stats.recordEviction();
38 return Optional.empty();
39 }
40 entry.recordHit(); // package-private method
41 stats.recordHit();
42 return Optional.of(entry.value); // package-private field
43 }
44
45 public void remove(String key) {
46 if (store.remove(key) != null) {
47 stats.recordEviction();
48 }
49 }
50
51 public String getStats() {
52 return stats.toString(); // package-private class method
53 }
54
55 public int size() { return store.size(); }
56
57 private void evictOldest() {
58 store.keySet().stream().findFirst()
59 .ifPresent(k -> {
60 store.remove(k);
61 stats.recordEviction();
62 });
63 }
64}1// File: com/devstackflow/demo/CacheDemo.java
2package com.devstackflow.demo;
3
4import com.devstackflow.cache.LocalCache;
5
6public class CacheDemo {
7
8 public static void main(String[] args) throws InterruptedException {
9
10 LocalCache cache = new LocalCache(5, 3); // max 5 entries, 3 sec TTL
11
12 cache.put("user:101", "Priya Sharma");
13 cache.put("user:102", "Rohan Mehta");
14 cache.put("prod:001", "Laptop");
15
16 System.out.println("Get user:101 : " + cache.get("user:101").orElse("MISS"));
17 System.out.println("Get user:102 : " + cache.get("user:102").orElse("MISS"));
18 System.out.println("Get user:999 : " + cache.get("user:999").orElse("MISS"));
19 System.out.println("Get user:101 : " + cache.get("user:101").orElse("MISS")); // hit again
20 System.out.println("Cache size : " + cache.size());
21 System.out.println("Stats : " + cache.getStats());
22
23 // CacheEntry and CacheStats are invisible here:
24 // new CacheEntry(...) ← compile error — package-private class
25 // new CacheStats() ← compile error — package-private class
26 // cache.stats ← compile error — private field (CacheStats itself package-private)
27 }
28}Output:
Get user:101 : Priya Sharma
Get user:102 : Rohan Mehta
Get user:999 : MISS
Get user:101 : Priya Sharma
Cache size : 3
Stats : CacheStats{puts=3, hits=3, misses=1, evictions=0, hitRate=75.0%}
CacheEntry and CacheStats are package-private. LocalCache uses them directly — accessing fields like entry.hitCount, entry.value, and stats.totalHits without getters. The package trusts its own members. External callers only see LocalCache with its clean public interface.
3 — Default vs Inheritance: The Key Difference From protected
Default access does NOT extend through inheritance to subclasses in other packages. This is the most important distinction between default and protected.
1// File: com/devstackflow/base/Template.java
2package com.devstackflow.base;
3
4public class Template {
5
6 String defaultField = "default"; // package-private
7 protected String prot = "protected"; // protected
8 public String pub = "public"; // public
9
10 void defaultMethod() { System.out.println("default method in Template"); }
11 protected void protMethod() { System.out.println("protected method in Template"); }
12 public void pubMethod() { System.out.println("public method in Template"); }
13}1// File: com/devstackflow/child/ChildClass.java
2package com.devstackflow.child; // DIFFERENT package
3
4import com.devstackflow.base.Template;
5
6public class ChildClass extends Template {
7
8 public void demonstrate() {
9
10 // public — always accessible
11 System.out.println("public field : " + pub);
12 pubMethod();
13
14 // protected — accessible through inheritance in subclass
15 System.out.println("protected : " + prot);
16 protMethod();
17
18 // default — NOT accessible from different package, even in subclass
19 // System.out.println(defaultField); ← compile error
20 // defaultMethod(); ← compile error
21 // Default access does NOT cross package boundaries via inheritance
22
23 System.out.println("Default members — NOT accessible from different package.");
24 }
25}1// File: com/devstackflow/demo/InheritanceDefaultDemo.java
2package com.devstackflow.demo;
3
4import com.devstackflow.child.ChildClass;
5
6public class InheritanceDefaultDemo {
7
8 public static void main(String[] args) {
9
10 ChildClass child = new ChildClass();
11 child.demonstrate();
12
13 // From completely outside — only public accessible
14 System.out.println("\nFrom non-subclass:");
15 System.out.println("public : " + child.pub);
16 child.pubMethod();
17 // child.prot ← compile error — protected, not subclass
18 // child.defaultField ← compile error — default, different package
19 }
20}Output:
public field : public
public method in Template
protected : protected
protected method in Template
Default members — NOT accessible from different package.
From non-subclass:
public : public
public method in Template
All Four Access Modifiers — Complete Comparison Table
| Aspect | private | default | protected | public |
|---|---|---|---|---|
| Same class | ✓ | ✓ | ✓ | ✓ |
| Same package — different class | ✗ | ✓ | ✓ | ✓ |
| Subclass in same package | ✗ | ✓ | ✓ | ✓ |
| Subclass in different package | ✗ | ✗ | ✓ | ✓ |
| Non-subclass in different package | ✗ | ✗ | ✗ | ✓ |
| Access via inheritance (cross-package) | ✗ | ✗ | ✓ | ✓ |
| Valid on top-level class | ✗ | ✓ | ✗ | ✓ |
| Valid on members | ✓ | ✓ | ✓ | ✓ |
| Keyword required | private | none | protected | public |
| Primary use | Class internals | Package module | Extensibility hooks | External API |
| Intended audience | This class only | Package team | Subclass authors | All callers |
| Risk if overused | Under-sharing | Tight package coupling | Fragile inheritance | Broken encapsulation |
The Package as a Module — Designing With default
The most powerful use of default access is designing a package as a self-contained module with a minimal public surface.
Package as a module — com.myapp.order:
External API (public): Internal implementation (default):
───────────────────────────── ──────────────────────────────────
public OrderService class OrderRepository
public OrderRequest class OrderValidator
public OrderResponse class OrderMapper
public OrderNotFoundException class OrderIdGenerator
class OrderAuditLogger
External callers see and use Package classes share freely
only the public surface. among themselves.
Changing the internal classes Changing the public surface
never breaks external callers. requires updating all callers.
The default classes are free The public classes are a
to be refactored, renamed, commitment that callers
split, or merged at any time. depend on.
Real-World Example — Order Processing Module
The Business Problem
An order processing module at a platform like Meesho has multiple internal classes — a mapper that converts request data, a repository that simulates database access, an ID generator, and a validator. Only OrderService and the data model classes are public. The rest are package-private implementation details that can evolve freely without affecting any external caller.
1// File: com/meesho/order/model/OrderRequest.java
2package com.meesho.order.model;
3
4public class OrderRequest { // public — external teams create these
5 private final String customerId;
6 private final String productId;
7 private final int quantity;
8 private final double unitPrice;
9
10 public OrderRequest(String customerId, String productId,
11 int quantity, double unitPrice) {
12 this.customerId = customerId;
13 this.productId = productId;
14 this.quantity = quantity;
15 this.unitPrice = unitPrice;
16 }
17
18 public String getCustomerId() { return customerId; }
19 public String getProductId() { return productId; }
20 public int getQuantity() { return quantity; }
21 public double getUnitPrice() { return unitPrice; }
22 public double getTotalAmount(){ return unitPrice * quantity; }
23}1// File: com/meesho/order/model/OrderRecord.java
2package com.meesho.order.model;
3
4import java.time.LocalDateTime;
5
6public class OrderRecord { // public — callers receive these
7 public final String orderId;
8 public final String customerId;
9 public final String productId;
10 public final double totalAmount;
11 public final String status;
12 public final LocalDateTime createdAt;
13
14 // package-private constructor — only order package creates OrderRecord
15 OrderRecord(String orderId, String customerId, String productId,
16 double totalAmount, String status) {
17 this.orderId = orderId;
18 this.customerId = customerId;
19 this.productId = productId;
20 this.totalAmount = totalAmount;
21 this.status = status;
22 this.createdAt = LocalDateTime.now();
23 }
24
25 @Override
26 public String toString() {
27 return String.format("[%s] %s | product=%s | Rs.%.2f | %s",
28 orderId, customerId, productId, totalAmount, status);
29 }
30}1// File: com/meesho/order/OrderIdGenerator.java
2package com.meesho.order;
3
4import java.util.concurrent.atomic.AtomicInteger;
5
6// package-private — only the order package generates IDs
7class OrderIdGenerator {
8 private static final AtomicInteger counter = new AtomicInteger(1000);
9
10 static String generate() {
11 return "MSH-ORD-" + String.format("%06d", counter.getAndIncrement());
12 }
13}1// File: com/meesho/order/OrderValidator.java
2package com.meesho.order;
3
4import com.meesho.order.model.OrderRequest;
5
6// package-private — validation is an internal concern
7class OrderValidator {
8
9 private static final int MAX_QUANTITY = 50;
10 private static final double MAX_AMOUNT = 50_000.0;
11
12 ValidationResult validate(OrderRequest request) {
13 if (request == null)
14 return ValidationResult.fail("Request cannot be null.");
15 if (request.getCustomerId() == null || request.getCustomerId().isBlank())
16 return ValidationResult.fail("Customer ID is required.");
17 if (request.getProductId() == null || request.getProductId().isBlank())
18 return ValidationResult.fail("Product ID is required.");
19 if (request.getQuantity() <= 0 || request.getQuantity() > MAX_QUANTITY)
20 return ValidationResult.fail("Quantity must be 1-" + MAX_QUANTITY + ".");
21 if (request.getTotalAmount() > MAX_AMOUNT)
22 return ValidationResult.fail("Order total exceeds limit of Rs." + MAX_AMOUNT + ".");
23 return ValidationResult.ok();
24 }
25}1// File: com/meesho/order/ValidationResult.java
2package com.meesho.order;
3
4// package-private — internal result type
5class ValidationResult {
6 final boolean valid;
7 final String error;
8
9 private ValidationResult(boolean valid, String error) {
10 this.valid = valid;
11 this.error = error;
12 }
13
14 static ValidationResult ok() { return new ValidationResult(true, null); }
15 static ValidationResult fail(String e) { return new ValidationResult(false, e); }
16}1// File: com/meesho/order/OrderRepository.java
2package com.meesho.order;
3
4import com.meesho.order.model.OrderRecord;
5import java.util.ArrayList;
6import java.util.List;
7import java.util.Optional;
8
9// package-private — data access is an internal concern
10class OrderRepository {
11
12 private final List<OrderRecord> store = new ArrayList<>();
13
14 void save(OrderRecord record) {
15 store.add(record);
16 }
17
18 Optional<OrderRecord> findById(String orderId) {
19 return store.stream()
20 .filter(r -> r.orderId.equals(orderId))
21 .findFirst();
22 }
23
24 List<OrderRecord> findByCustomer(String customerId) {
25 return store.stream()
26 .filter(r -> r.customerId.equals(customerId))
27 .collect(java.util.stream.Collectors.toList());
28 }
29
30 int count() { return store.size(); }
31}1// File: com/meesho/order/OrderService.java
2package com.meesho.order;
3
4import com.meesho.order.model.OrderRecord;
5import com.meesho.order.model.OrderRequest;
6
7import java.util.List;
8import java.util.Optional;
9
10// public — this is the only entry point for external callers
11public class OrderService {
12
13 // Package-private helpers — freely instantiated within the package
14 private final OrderValidator validator = new OrderValidator();
15 private final OrderRepository repository = new OrderRepository();
16
17 public OrderRecord placeOrder(OrderRequest request) {
18 ValidationResult result = validator.validate(request); // default class
19 if (!result.valid) { // default field
20 throw new IllegalArgumentException(result.error); // default field
21 }
22
23 String orderId = OrderIdGenerator.generate(); // default static method
24 OrderRecord record = new OrderRecord( // default constructor
25 orderId,
26 request.getCustomerId(),
27 request.getProductId(),
28 request.getTotalAmount(),
29 "PLACED");
30
31 repository.save(record); // default method
32 return record;
33 }
34
35 public Optional<OrderRecord> getOrder(String orderId) {
36 return repository.findById(orderId);
37 }
38
39 public List<OrderRecord> getCustomerOrders(String customerId) {
40 return repository.findByCustomer(customerId);
41 }
42
43 public int getTotalOrders() {
44 return repository.count();
45 }
46}1// File: com/meesho/OrderApp.java
2package com.meesho;
3
4import com.meesho.order.OrderService;
5import com.meesho.order.model.OrderRecord;
6import com.meesho.order.model.OrderRequest;
7
8public class OrderApp {
9
10 public static void main(String[] args) {
11
12 System.out.println("╔══════════════════════════════════════════╗");
13 System.out.println("║ MEESHO ORDER MODULE DEMO ║");
14 System.out.println("╚══════════════════════════════════════════╝\n");
15
16 OrderService service = new OrderService();
17
18 // Place orders
19 System.out.println("=== Placing Orders ===");
20 OrderRecord r1 = service.placeOrder(
21 new OrderRequest("CUST-101", "PROD-A", 2, 699.0));
22 OrderRecord r2 = service.placeOrder(
23 new OrderRequest("CUST-102", "PROD-B", 1, 1299.0));
24 OrderRecord r3 = service.placeOrder(
25 new OrderRequest("CUST-101", "PROD-C", 3, 499.0));
26
27 System.out.println(r1);
28 System.out.println(r2);
29 System.out.println(r3);
30
31 // Query orders
32 System.out.println("\n=== Orders for CUST-101 ===");
33 service.getCustomerOrders("CUST-101")
34 .forEach(System.out::println);
35
36 System.out.println("\n=== Get by ID ===");
37 service.getOrder(r2.orderId)
38 .ifPresent(System.out::println);
39
40 System.out.println("\nTotal orders: " + service.getTotalOrders());
41
42 // Validation failure
43 System.out.println("\n=== Validation Failure ===");
44 try {
45 service.placeOrder(new OrderRequest("", "PROD-D", 1, 299.0));
46 } catch (IllegalArgumentException e) {
47 System.out.println("Error: " + e.getMessage());
48 }
49
50 // These are invisible from outside the package:
51 // new OrderValidator() ← compile error — package-private class
52 // new OrderRepository() ← compile error — package-private class
53 // new OrderIdGenerator() ← compile error — package-private class
54 // OrderIdGenerator.generate() ← compile error — package-private class
55 // new OrderRecord(...) ← compile error — package-private constructor
56 }
57}Output:
╔══════════════════════════════════════════╗
║ MEESHO ORDER MODULE DEMO ║
╚══════════════════════════════════════════╝
=== Placing Orders ===
[MSH-ORD-001000] CUST-101 | product=PROD-A | Rs.1398.00 | PLACED
[MSH-ORD-001001] CUST-102 | product=PROD-B | Rs.1299.00 | PLACED
[MSH-ORD-001002] CUST-101 | product=PROD-C | Rs.1497.00 | PLACED
=== Orders for CUST-101 ===
[MSH-ORD-001000] CUST-101 | product=PROD-A | Rs.1398.00 | PLACED
[MSH-ORD-001002] CUST-101 | product=PROD-C | Rs.1497.00 | PLACED
=== Get by ID ===
[MSH-ORD-001001] CUST-102 | product=PROD-B | Rs.1299.00 | PLACED
Total orders: 3
=== Validation Failure ===
Error: Customer ID is required.
Six classes in the package — only OrderService, OrderRequest, and OrderRecord are public. OrderValidator, OrderRepository, OrderIdGenerator, ValidationResult, and the OrderRecord constructor are all package-private. External callers in com.meesho can see and use exactly what they need. The internal implementation is completely sealed.
Best Practices
Use default access intentionally, not by accident. Omitting a modifier is a deliberate design choice — it means "this belongs to the package". Write a conscious comment or follow a team convention that distinguishes intentional package-private from "I forgot to write public". Some teams prefix package-private classes with a comment /* package */ to make the intent explicit.
Design packages as cohesive modules with a small public surface. A well-designed package exposes a handful of public classes as its API. Everything else is default — internal validators, mappers, repositories, ID generators, result types. This modularity means the internal implementation can be refactored freely without any impact on callers.
Prefer default over protected for internal helpers that subclasses do not need. protected leaks members into the subclass space — every subclass author in any package can see and depend on them. If a helper method or class is only needed within the package and no subclass in a different package has a genuine need for it, keep it default. Reserve protected for intentional extensibility hooks.
Avoid putting unrelated classes in the same package just for default access. Default access is not a reason to create a sprawling package. If two groups of classes have default access to each other but conceptually belong to different layers or domains, they should be in separate packages with a cleaner, controlled interface between them.
Common Mistakes
Mistake 1 — Accidentally Making a Class Package-Private
1// WRONG — forgot public; this class is package-private unintentionally
2class UserController { // only visible in this package
3 public void handleLogin() { }
4}
5
6// This causes a cascade: any class in another package cannot import it,
7// Spring cannot inject it as a bean, APIs cannot map to it, etc.
8
9// Fix — add public if it is meant for external use
10public class UserController {
11 public void handleLogin() { }
12}Mistake 2 — Expecting Default to Cross Package Boundaries
1// com.myapp.base.Base
2package com.myapp.base;
3public class Base {
4 void helper() { System.out.println("base helper"); } // default
5}
6
7// com.myapp.child.Child
8package com.myapp.child;
9import com.myapp.base.Base;
10
11public class Child extends Base {
12 public void go() {
13 helper(); // compile error — default does not cross packages, even with inheritance
14 }
15}
16
17// Fix — make helper() protected if Child genuinely needs it
18// OR restructure so Child is in the same package as BaseMistake 3 — Using the Same Package to Avoid Defining Proper Interfaces
1// WRONG — shoving unrelated classes into one package so they share default access
2package com.myapp.everything; // UserService, ProductService, PaymentService, Inventory...
3// These are logically separate but all crammed together for default access
4
5// CORRECT — define interfaces between packages; keep packages cohesive
6// com.myapp.user → UserService (public), UserRepository (default)
7// com.myapp.product → ProductService (public), ProductRepository (default)
8// Each package is a cohesive unit, not a dumping groundMistake 4 — Testing Package-Private Classes From a Different Package
1// Test class in wrong package:
2package com.test;
3import com.myapp.order.OrderValidator; // compile error — package-private
4
5// Fix 1 — put test in the SAME package
6package com.myapp.order; // same package — can access default members
7// But: most test conventions put tests in a separate source root; same package name is fine
8
9// Fix 2 — test through the public API (preferred approach)
10package com.test;
11import com.myapp.order.OrderService; // public — test via public service
12// OrderService internally uses OrderValidator — if it works, the validator worksInterview Questions
Q1. What is default (package-private) access in Java and how do you declare it?
Default access is the access level applied when no modifier keyword is written. It is declared by the absence of public, private, or protected. A class, field, method, or constructor with default access is visible to all classes within the same package and invisible to everything outside the package — including subclasses in different packages. The common name is "package-private" because the package is the boundary of its visibility.
Q2. What is the difference between default and protected access?
Within the same package, both default and protected provide identical access — all classes in the package can use the member. The difference is inheritance across packages: protected extends access to subclasses in any package; default does not. A subclass in a different package cannot access a default member of its parent class, even through super or this. default is strictly package-scoped; protected adds the subclass cross-package dimension.
Q3. Can a subclass in a different package inherit and access default members of its parent?
No. Default access does not cross package boundaries even through inheritance. A subclass in a different package cannot see default fields, call default methods, or override default methods of the parent. Attempting to do so causes a compile error. This is the key difference from protected, which does cross package boundaries specifically for subclasses. For a member to be accessible to subclasses in other packages, it must be protected or public.
Q4. Why is default access useful if you can just use private for everything?
private restricts access to one class. When multiple cooperating classes within a package need to share data or behaviour — a service and its validator, a processor and its mapper, a repository and its result type — private forces every interaction to go through public getters and setters, adding unnecessary boilerplate. Default access trusts the package as a cohesive unit. Classes within the same package can share fields and methods directly, without the overhead of a public API. This is how packages act as internal modules.
Q5. Is it a problem to leave the access modifier off by accident?
Yes. Accidentally omitting public from a class that is meant to be part of the external API makes it package-private — invisible and unimportable from other packages. This causes compile errors for callers, breaks Spring and Hibernate bean scanning (which cannot instantiate package-private classes), and produces subtle test failures when tests are in a different package. Always be intentional about access modifiers. If a class is meant for external use, write public explicitly. If it is an internal helper, omit the modifier deliberately and — optionally — add a comment to signal the intent.
Q6. Can default access members be tested in JUnit?
Yes, with the right package structure. JUnit test classes in the same package as the class under test have full access to default members — fields, methods, and constructors. Maven and Gradle's standard layout places test source files in src/test/java but preserves the same package name. A test class com.myapp.order.OrderValidatorTest in src/test/java has access to class OrderValidator in src/main/java/com/myapp/order/ because both share the com.myapp.order package on the classpath. This is the standard approach for testing package-private implementation classes directly.
FAQs
Is there a way to explicitly write the "default" keyword in Java?
No. Default access has no keyword. It is declared by the complete absence of any access modifier. Some teams use a comment /* package */ before the class or member to signal intentional package-private choice — /* package */ class OrderValidator { } — but this is a convention, not a language feature.
Can a package-private class implement a public interface?
Yes. A package-private class can implement any public interface. The implementing class is invisible outside the package, but objects of that class can be returned as the interface type from a public factory method. This is a powerful pattern: the interface is public, the implementation is package-private. External callers only know the interface — they cannot instantiate or cast to the implementation directly.
Does default access apply to constructors separately from the class?
Yes. A public class can have a package-private constructor — public class Foo { Foo() {} }. The class is importable from anywhere, but only code in the same package can call new Foo(). External callers are forced to use a public factory method or builder. OrderRecord in the real-world example used exactly this pattern — public class with a package-private constructor, so only the order package can create instances.
Are there any frameworks that depend on package-private access specifically?
Yes. Several Java testing libraries use package-private access for their internal structure. The Java module system relies heavily on the concept of not exporting packages — unexported packages are effectively package-private to external modules. OSGi bundles use package-private visibility as a module boundary. JPA (Hibernate) entity classes often use package-private constructors for the framework while providing public constructors for application code.
What happens to default access when using the Java module system (Java 9+)?
The Java module system (module-info.java) adds a layer above packages. Even public classes in a module are inaccessible to other modules if the package is not listed in an exports directive. Default access still works within the module as always — package-private classes are visible within their package. The module system's encapsulation is stronger than package access: it prevents reflection-based access to non-exported packages by default. Default access within a module is unchanged; it is the cross-module visibility that modules restrict.
Summary
Default (package-private) access is Java's mechanism for package-level modularity. Members with no modifier are visible to all classes within the same package and invisible to everything outside — including subclasses in other packages, which distinguishes it from protected.
Its power lies in enabling the package-as-module pattern: a small public surface for external callers, a rich set of default helpers that cooperate freely within the package, and no leakage of implementation details to the outside world. The internal classes — validators, mappers, repositories, ID generators, result types — are free to change, refactor, or be replaced entirely. Callers are insulated because they only ever depended on the public surface.
The discipline: be intentional. Write public when something is meant for external use. Write private when it belongs to one class. Omit the modifier deliberately when it should be shared within the package. Never leave it off by accident on something that should be public.
What to Read Next
Learn how to define a class inside another class.