Java Tutorial
🔍

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

Aspectprivatedefaultprotectedpublic
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 requiredprivatenoneprotectedpublic
Primary useClass internalsPackage moduleExtensibility hooksExternal API
Intended audienceThis class onlyPackage teamSubclass authorsAll callers
Risk if overusedUnder-sharingTight package couplingFragile inheritanceBroken 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 Base

Mistake 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 ground

Mistake 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 works

Interview 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