Loading content...
Public, private, and protected exist in nearly every object-oriented language. But modern programming languages recognize that these three levels aren't always enough. Sometimes you need visibility that spans multiple classes working closely together — without exposing them to the entire outside world.
Enter package-private (Java), internal (C#), module (Kotlin/Swift), and various other language-specific access levels. These modifiers create visibility boundaries at a higher level than the class — at the package, module, or assembly level.
By the end of this page, you will understand: • Java's default (package-private) access and when to use it • C#'s internal modifier and the assembly concept • Kotlin and Swift's module-based visibility • How package/module visibility enables encapsulation at scale • Design patterns that leverage package-level access • Best practices for organizing code into packages/modules
In Java, when you declare a member without any access modifier, it has package-private (also called "default") access. This means it's visible to all classes in the same package, but invisible to classes in other packages.
Package-Private Visibility:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
// File: com/myapp/user/UserRepository.javapackage com.myapp.user; // Public class - accessible from anywherepublic class UserRepository { // Package-private field - accessible only in com.myapp.user List<User> cachedUsers; // Package-private method - used by sibling classes void invalidateCache() { cachedUsers = null; } // Public method - part of the external API public User findById(String id) { if (cachedUsers != null) { return findInCache(id); } return loadFromDatabase(id); } // Package-private - other classes in package can call User loadFromDatabase(String id) { // Database access logic return new User(id); } // Private - only this class private User findInCache(String id) { return cachedUsers.stream() .filter(u -> u.getId().equals(id)) .findFirst() .orElse(null); }} // File: com/myapp/user/UserService.java (SAME PACKAGE)package com.myapp.user; public class UserService { private UserRepository repository = new UserRepository(); public void refreshUser(String id) { // ✅ Can access package-private method repository.invalidateCache(); // ✅ Can access package-private method User user = repository.loadFromDatabase(id); // ❌ Cannot access private method // repository.findInCache(id); // Error! }} // File: com/myapp/order/OrderService.java (DIFFERENT PACKAGE)package com.myapp.order; import com.myapp.user.UserRepository; public class OrderService { private UserRepository userRepo = new UserRepository(); public void processOrder(String userId) { // ✅ Can access public method User user = userRepo.findById(userId); // ❌ Cannot access package-private members // userRepo.invalidateCache(); // Error! // userRepo.loadFromDatabase(userId); // Error! // userRepo.cachedUsers; // Error! }}Package-private access treats the package as a unit of encapsulation. Within a package, classes collaborate freely. Across packages, only public (and protected through inheritance) is visible. This enables internal implementation sharing while maintaining clean external APIs.
Package-private access fills a specific need: sharing implementation between related classes without exposing it to the world. It's surprisingly powerful and underutilized.
Primary Use Cases:
| Use Case | Description | Example |
|---|---|---|
| Implementation Classes | Helpers that support public classes but shouldn't be used directly | UserValidator used by UserService, not exported |
| Test Access | Allow test classes (in same package) to access internals | Tests can call package-private setup methods |
| Sibling Collaboration | Classes that implement a feature together | OrderProcessor and OrderValidator sharing state |
| Factory Internals | Concrete classes created by public factories | Private implementations behind public interface |
| Package Cohesion | Force all usage through a single public entry point | One public facade, many package-private workers |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
// Package: com.payments // PUBLIC: The only entry point for external codepublic class PaymentGateway { // Package-private dependencies - wired internally private final CardValidator cardValidator = new CardValidator(); private final FraudDetector fraudDetector = new FraudDetector(); private final TransactionProcessor processor = new TransactionProcessor(); public PaymentResult processPayment(PaymentRequest request) { ValidationResult validation = cardValidator.validate(request.getCard()); if (!validation.isValid()) { return PaymentResult.invalid(validation.getErrors()); } RiskAssessment risk = fraudDetector.assess(request); if (risk.isHighRisk()) { return PaymentResult.rejected("Flagged for fraud review"); } return processor.process(request); }} // PACKAGE-PRIVATE: Implementation detail - not public APIclass CardValidator { ValidationResult validate(Card card) { List<String> errors = new ArrayList<>(); if (!isValidLuhn(card.getNumber())) { errors.add("Invalid card number"); } if (isExpired(card.getExpiry())) { errors.add("Card expired"); } return new ValidationResult(errors); } // Package-private - used by test classes boolean isValidLuhn(String cardNumber) { // Luhn algorithm implementation return true; } private boolean isExpired(YearMonth expiry) { return expiry.isBefore(YearMonth.now()); }} // PACKAGE-PRIVATE: Another internal componentclass FraudDetector { RiskAssessment assess(PaymentRequest request) { double riskScore = calculateRiskScore(request); return new RiskAssessment(riskScore); } // Package-private for testing double calculateRiskScore(PaymentRequest request) { // Complex scoring logic return 0.1; }} // PACKAGE-PRIVATE: Transaction processingclass TransactionProcessor { PaymentResult process(PaymentRequest request) { // Actual payment processing return PaymentResult.success(generateTransactionId()); } String generateTransactionId() { return UUID.randomUUID().toString(); }} // External code sees ONLY PaymentGateway:// PaymentGateway gateway = new PaymentGateway();// gateway.processPayment(request); // ✅ Works// CardValidator validator = new CardValidator(); // ❌ Error: CardValidator is not publicA powerful pattern: when your test classes are in the same package as the production code (but in a different source folder like src/test/java), they can access package-private members. This lets you test internal logic without making it public.
// Production code: src/main/java/com/app/Calculator.java
class Calculator {
int internalAdd(int a, int b) { return a + b; }
}
// Test code: src/test/java/com/app/CalculatorTest.java
class CalculatorTest {
@Test void testInternalAdd() {
Calculator calc = new Calculator();
assertEquals(5, calc.internalAdd(2, 3)); // ✅ Works!
}
}
C# uses internal for assembly-level visibility. An assembly in C# is a compiled unit — typically a DLL or EXE file. Internal members are visible within the entire assembly but hidden from other assemblies.
Key Difference from Java:
Java's packages are compile-time constructs; C#'s assemblies are deployment units. This means internal visibility is enforced at the binary level, not just the source level.
C# Access Modifier Spectrum:
| Modifier | Visibility | Use Case |
|---|---|---|
| public | Everywhere | Public API |
| private | Declaring class only | Implementation details |
| protected | Class + subclasses | Inheritance hooks |
| internal | Same assembly | Implementation shared across namespaces |
| protected internal | Same assembly OR subclasses | Flexible framework extension |
| private protected | Same assembly AND subclass | Strict internal inheritance |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
// File: MyLibrary/PaymentModule/PaymentService.csnamespace MyLibrary.PaymentModule{ // Public class - exported from the assembly public class PaymentService { // Internal: visible within MyLibrary.dll, not to consumers internal static readonly ILogger Logger = LoggerFactory.Create(); private readonly IPaymentProcessor _processor; public PaymentService(IPaymentProcessor processor) { _processor = processor; } public PaymentResult ProcessPayment(PaymentRequest request) { // Using internal member Logger.LogInfo("Processing payment: {}", request.Id); return _processor.Process(request); } }} // File: MyLibrary/Utilities/InternalHelper.csnamespace MyLibrary.Utilities{ // Internal class - cannot be seen outside MyLibrary.dll internal class InternalHelper { internal static string FormatCurrency(decimal amount) { return $"${amount: N2 }"; } internal static bool IsValidEmail(string email) { return email.Contains("@") && email.Contains("."); } }} // File: MyLibrary/ValidationModule/Validator.csnamespace MyLibrary.ValidationModule{ public class Validator { // Internal from different namespace - works! Same assembly. public bool ValidateEmail(string email) { // ✅ Can access InternalHelper from Utilities namespace return Utilities.InternalHelper.IsValidEmail(email); } }} // File: ConsumerApp/Program.cs (DIFFERENT ASSEMBLY)using MyLibrary.PaymentModule;using MyLibrary.ValidationModule; namespace ConsumerApp { class Program { static void Main() { // ✅ Can use public classes var service = new PaymentService(new MockProcessor()); var validator = new Validator(); // ❌ Cannot access internal members // PaymentService.Logger.LogInfo("..."); // Error! // MyLibrary.Utilities.InternalHelper.FormatCurrency(100); // Error! } }} C# has a great testing feature: InternalsVisibleTo. Add this to your library's AssemblyInfo.cs:
[assembly: InternalsVisibleTo("MyLibrary.Tests")]
Now your test assembly can access all internal members! This is the idiomatic way to test internal code in C# — much cleaner than reflection hacks.
Modern languages like Kotlin and Swift have refined the module concept, making it a first-class visibility boundary.
Kotlin's Approach:
Kotlin uses internal for module visibility. In Kotlin, a module is a set of source files compiled together:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
// Module: payment-core // Internal class - visible within payment-core module onlyinternal class PaymentValidator { fun validate(request: PaymentRequest): ValidationResult { return if (isValid(request)) { ValidationResult.Valid } else { ValidationResult.Invalid("Invalid payment data") } } internal fun isValid(request: PaymentRequest): Boolean { return request.amount > 0 && request.currency.isNotBlank() }} // Public class - the exported APIclass PaymentProcessor { // Private - only PaymentProcessor uses this private val validator = PaymentValidator() // Public - external modules call this fun processPayment(request: PaymentRequest): PaymentResult { val validation = validator.validate(request) return when (validation) { is ValidationResult.Valid -> executePayment(request) is ValidationResult.Invalid -> PaymentResult.Error(validation.reason) } } // Internal - other classes in payment-core can access internal fun executePayment(request: PaymentRequest): PaymentResult { // Core payment logic return PaymentResult.Success(generateId()) } private fun generateId(): String = java.util.UUID.randomUUID().toString()} // Another file in payment-core (same module)internal class PaymentLogger { fun log(processor: PaymentProcessor, request: PaymentRequest) { // ✅ Can access internal member processor.executePayment(request) }} // In consumer module (different Gradle project):// val validator = PaymentValidator() // ❌ Error: internal// processor.executePayment(request) // ❌ Error: internal// processor.processPayment(request) // ✅ Works: public12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
// Swift has five access levels (most restrictive to least):// private, fileprivate, internal (default), public, open // Module: PaymentFramework // Internal (default) - visible within PaymentFramework moduleclass PaymentValidator { func validate(_ request: PaymentRequest) -> Bool { return request.amount > 0 }} // Public - visible to importers, but NOT subclassable outside modulepublic class PaymentProcessor { private let validator = PaymentValidator() public init() {} // Public: callable from outside the module public func process(_ request: PaymentRequest) -> PaymentResult { guard validator.validate(request) else { return .failure("Invalid request") } return execute(request) } // Internal: other PaymentFramework code can call this func execute(_ request: PaymentRequest) -> PaymentResult { return .success(generateId()) } // Fileprivate: only code in THIS FILE can access fileprivate func generateId() -> String { return UUID().uuidString } // Private: only this class can access private let config = Configuration()} // Open: can be subclassed AND overridden outside the moduleopen class ExtensibleProcessor: PaymentProcessor { open func customProcess(_ request: PaymentRequest) -> PaymentResult { // Subclasses outside the module can override this return process(request) }} // Consumer code (different module):import PaymentFramework let processor = PaymentProcessor()processor.process(request) // ✅ Public// processor.execute(request) // ❌ Internal// PaymentValidator() // ❌ Internal class class MyProcessor: ExtensibleProcessor { override func customProcess(_ request: PaymentRequest) -> PaymentResult { // ✅ Can override: method is 'open' return super.customProcess(request) } // override func process(...) // ❌ Can't override: 'public' not 'open'}Swift distinguishes between public and open:
• public: Visible outside the module but cannot be subclassed/overridden outside • open: Visible AND can be subclassed/overridden outside the module
This gives library authors control over what clients can extend. Use open only for intentional extension points.
Package and module boundaries are architectural decisions. How you organize code into packages determines what can see what, affecting maintainability and API design.
Core Principles:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
// Feature-based package organization // com.myapp.user package// └── User.java (public) - Domain entity// └── UserRepository.java (public) - Data access interface// └── UserService.java (public) - External API// └── UserValidator.java (package) - Internal validation// └── UserRepositoryImpl.java (package) - Internal implementation// └── UserMapper.java (package) - Internal DTO mapping package com.myapp.user; // PUBLIC: The interface external packages depend onpublic interface UserRepository { User findById(String id); void save(User user); List<User> findAll();} // PUBLIC: The external service APIpublic class UserService { private final UserRepository repository; private final UserValidator validator; // Package-private dependency public UserService(UserRepository repository) { this.repository = repository; this.validator = new UserValidator(); } public User createUser(CreateUserRequest request) { validator.validate(request); // Internal validation User user = UserMapper.toEntity(request); // Internal mapping repository.save(user); return user; }} // PACKAGE-PRIVATE: Implementation detailclass UserValidator { void validate(CreateUserRequest request) { if (request.getEmail() == null || !request.getEmail().contains("@")) { throw new ValidationException("Invalid email"); } if (request.getName() == null || request.getName().isBlank()) { throw new ValidationException("Name required"); } }} // PACKAGE-PRIVATE: Implementation detailclass UserRepositoryImpl implements UserRepository { private final DataSource dataSource; UserRepositoryImpl(DataSource dataSource) { this.dataSource = dataSource; } @Override public User findById(String id) { // JDBC implementation return null; } // ...} // PACKAGE-PRIVATE: Internal utilityclass UserMapper { static User toEntity(CreateUserRequest request) { return new User(request.getName(), request.getEmail()); } static UserDTO toDTO(User user) { return new UserDTO(user.getId(), user.getName(), user.getEmail()); }}Avoid organizing by layer (controllers, services, repositories). This scatters related code:
❌ com.app.controllers.UserController
❌ com.app.services.UserService
❌ com.app.repositories.UserRepository
Each layer becomes a package with many unrelated classes, and everything must be public because packages can't share internal members.
✅ Instead: com.app.user.{UserController, UserService, UserRepository}
Let's consolidate how different languages handle the "more than class, less than public" visibility scope.
Comprehensive Comparison:
| Language | Keyword | Boundary | Test Access Strategy |
|---|---|---|---|
| Java | (no keyword) | Package | Test in same package different folder |
| C# | internal | Assembly (DLL) | InternalsVisibleTo attribute |
| Kotlin | internal | Compilation module | Same module, different source set |
| Swift | (default) | Framework/Module | Same module, different target |
| TypeScript | N/A | No equivalent | No true private/internal enforcement |
| Python | _ prefix (convention) | Module (file) | No enforcement, convention only |
| Go | lowercase | Package | Test in same package (*_test.go) |
| Rust | pub(crate) | Crate | Tests in same crate |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
// TypeScript: No true internal access (everything can be imported)// Workaround: Use barrels (index.ts) to control exports // src/payments/internal/processor.tsexport class PaymentProcessor { // Technically exportable process(request: PaymentRequest): PaymentResult { return { success: true }; }} // src/payments/internal/validator.tsexport class PaymentValidator { // Technically exportable validate(request: PaymentRequest): boolean { return true; }} // src/payments/index.ts (barrel file)// Only export what should be publicexport { PaymentProcessor } from './internal/processor';// PaymentValidator is NOT exported - effectively "internal" // Consumer must import from the barrel:import { PaymentProcessor } from '@mylib/payments';// import { PaymentValidator } from '@mylib/payments'; // Error: not exported// import { PaymentValidator } from '@mylib/payments/internal/validator'; // Works but linted against // ============================================ # Python: Convention-based internal marking # payments/processor.pyclass PaymentProcessor: """Public API class.""" def process(self, request): validator = _InternalValidator() # Internal class if validator.validate(request): return self._execute(request) # Internal method return {"error": "Invalid request"} def _execute(self, request): """Internal method - indicated by single underscore.""" return {"success": True} class _InternalValidator: """Internal class - indicated by leading underscore.""" def validate(self, request): return request.get("amount", 0) > 0 # payments/__init__.py# Control what's publicly exportedfrom .processor import PaymentProcessor# _InternalValidator is NOT imported - conventionally internal # Usage:from payments import PaymentProcessor # ✅ Worksfrom payments.processor import _InternalValidator # Works but discouragedChoosing the right visibility for each member is a design decision that affects your codebase's maintainability. Here's a systematic approach:
The Visibility Decision Framework:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
public class Order { // Question: Who needs to access this? // PRIVATE: Only Order uses these for internal state private final String orderId; private final List<OrderItem> items; private OrderStatus status; private BigDecimal calculatedTotal; // PRIVATE: Internal validation logic private void validateItems(List<OrderItem> items) { if (items == null || items.isEmpty()) { throw new IllegalArgumentException("Order must have items"); } } // PACKAGE-PRIVATE: OrderService in same package manages orders void transitionTo(OrderStatus newStatus) { if (!this.status.canTransitionTo(newStatus)) { throw new IllegalStateException("Invalid transition"); } this.status = newStatus; } // PACKAGE-PRIVATE: Accessed by OrderPriceCalculator in same package BigDecimal getSubtotal() { return items.stream() .map(OrderItem::getLineTotal) .reduce(BigDecimal.ZERO, BigDecimal::add); } // PROTECTED: Would be used if Order is subclassed (rare for entities) // Typically entities aren't subclassed, so this is uncommon // PUBLIC: The official API clients use public String getOrderId() { return orderId; } public OrderStatus getStatus() { return status; } public BigDecimal getTotal() { return calculatedTotal; } public List<OrderItem> getItems() { // Defensive copy: internal list stays protected return List.copyOf(items); } public void addItem(Product product, int quantity) { // Validates and adds, maintaining encapsulation } // Constructor: could be public or package-private depending on creation patterns public Order(String orderId, List<OrderItem> items) { validateItems(items); this.orderId = orderId; this.items = new ArrayList<>(items); // Defensive copy this.status = OrderStatus.PENDING; recalculateTotal(); } private void recalculateTotal() { this.calculatedTotal = getSubtotal(); // Uses package-private method }}If you're unsure between two access levels, choose the more restrictive one. You can always loosen access later without breaking anything. Tightening access after the fact breaks every caller that depended on the looser access.
We've completed our comprehensive exploration of access modifiers — from public's wide-open visibility to private's strict confinement, through protected's inheritance bridge, to package/internal's broader encapsulation units. Let's consolidate the complete picture:
| Access Level | Use When | Avoid When |
|---|---|---|
| public | External code must call it | It's implementation detail |
| private | Only this class uses it | Subclasses need it |
| protected | Subclasses customize behavior | No inheritance is intended |
| package/internal | Same-package classes collaborate | It's truly class-internal |
What's Next:
You now have complete mastery of access modifiers — the fundamental mechanism for controlling visibility in object-oriented systems. This knowledge forms the foundation for the next chapter, where we'll explore static vs instance members — understanding when behavior belongs to the class versus individual objects.
You've mastered access modifiers — one of the most important aspects of object-oriented design. You understand not just the syntax, but the why behind each visibility level. This knowledge enables you to design clean, maintainable, and appropriately encapsulated systems. Every class you design from now on will benefit from this understanding.