Loading learning content...
We've explored the principles of encapsulation—bundling data with behavior, hiding implementation details, controlling access through visibility modifiers. But principles without practice remain abstract. This page bridges that gap by walking through comprehensive code examples that demonstrate encapsulation in action.
The progression we'll follow:
For each domain, we'll examine code that violates encapsulation principles, understand why it fails, and then refactor it into a well-encapsulated design. This before-and-after approach reveals not just what good encapsulation looks like, but why each change matters.
By the end of this page, you will be able to recognize encapsulation violations in real code, understand the refactoring patterns that fix them, and apply these patterns to your own designs. You'll see encapsulation not as an abstract concept but as a concrete set of code transformation techniques.
The bank account is a canonical example for encapsulation because financial operations have strict invariants that must be maintained. Let's examine a poorly designed version and systematically improve it.
Consider this naive implementation that exposes its internals:
12345678910111213141516171819202122232425262728293031
// ❌ POOR ENCAPSULATION - Everything is exposedpublic class BankAccount { // Public fields allow unrestricted access public String accountNumber; public String accountHolderName; public double balance; public List<String> transactionHistory; public BankAccount(String accountNumber, String name) { this.accountNumber = accountNumber; this.accountHolderName = name; this.balance = 0.0; this.transactionHistory = new ArrayList<>(); }} // Client code can do anythingclass Client { public void problematicUsage(BankAccount account) { // Direct field manipulation bypasses all validation account.balance = -5000; // Negative balance? No problem! account.balance = account.balance * 2; // Double the money! // Account number can be changed arbitrarily account.accountNumber = "FAKE123456"; // Transaction history can be modified or deleted account.transactionHistory.clear(); // Evidence destroyed! account.transactionHistory.add("Fake deposit: $1,000,000"); }}This code has no protection whatsoever. Any client can directly modify the balance (including setting it negative), change the account number after creation, delete or fabricate transaction history, and bypass all business rules. This isn't just bad design—in financial software, it's a security disaster that could enable fraud.
Now let's see proper encapsulation applied systematically:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
// ✅ PROPER ENCAPSULATION - Controlled access, enforced invariantspublic final class BankAccount { // All fields are private - no direct access possible private final String accountNumber; // Immutable after creation private final String accountHolderName; // Immutable identity private double balance; private final List<Transaction> transactionHistory; // Private nested class - implementation detail hidden from clients private static class Transaction { private final LocalDateTime timestamp; private final String type; private final double amount; private final double balanceAfter; Transaction(String type, double amount, double balanceAfter) { this.timestamp = LocalDateTime.now(); this.type = type; this.amount = amount; this.balanceAfter = balanceAfter; } } // Constructor enforces initial invariants public BankAccount(String accountNumber, String accountHolderName) { validateAccountNumber(accountNumber); validateAccountHolderName(accountHolderName); this.accountNumber = accountNumber; this.accountHolderName = accountHolderName; this.balance = 0.0; this.transactionHistory = new ArrayList<>(); } // Private validation - implementation detail private void validateAccountNumber(String accountNumber) { if (accountNumber == null || !accountNumber.matches("[A-Z]{2}\\d{8}")) { throw new IllegalArgumentException( "Account number must be 2 letters followed by 8 digits" ); } } private void validateAccountHolderName(String name) { if (name == null || name.trim().isEmpty()) { throw new IllegalArgumentException( "Account holder name cannot be empty" ); } } // Controlled operations - enforce business rules public void deposit(double amount) { if (amount <= 0) { throw new IllegalArgumentException("Deposit amount must be positive"); } this.balance += amount; recordTransaction("DEPOSIT", amount); } public void withdraw(double amount) { if (amount <= 0) { throw new IllegalArgumentException("Withdrawal amount must be positive"); } if (amount > this.balance) { throw new InsufficientFundsException( "Cannot withdraw " + amount + " from balance of " + balance ); } this.balance -= amount; recordTransaction("WITHDRAWAL", amount); } // Private helper - encapsulated implementation private void recordTransaction(String type, double amount) { transactionHistory.add(new Transaction(type, amount, this.balance)); } // Getters provide read-only access public String getAccountNumber() { return accountNumber; // String is immutable, safe to return } public String getAccountHolderName() { return accountHolderName; } public double getBalance() { return balance; // Primitive, safe to return } // Defensive copy prevents client modification of internal list public List<String> getTransactionHistory() { return transactionHistory.stream() .map(t -> String.format("[%s] %s: $%.2f (Balance: $%.2f)", t.timestamp.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), t.type, t.amount, t.balanceAfter)) .collect(Collectors.toUnmodifiableList()); }}final modifier ensures accountNumber and accountHolderName cannot change after construction.Session management is security-critical code where encapsulation directly impacts system safety. Poor encapsulation in sessions can lead to session hijacking, privilege escalation, and authentication bypass.
1234567891011121314151617181920212223242526272829303132333435363738
// ❌ SECURITY NIGHTMARE - Exposed internals enable attackspublic class UserSession { public String sessionId; public String userId; public boolean isAuthenticated; public boolean isAdmin; public Date expirationTime; public Map<String, Object> sessionData; public UserSession() { sessionData = new HashMap<>(); }} // Attack scenarios enabled by poor encapsulationclass Attacker { public void elevatePrivileges(UserSession session) { // Become admin without proper authorization session.isAdmin = true; session.isAuthenticated = true; } public void hijackSession(UserSession session) { // Steal session by changing the ID session.sessionId = "stolen-valid-session-id"; session.userId = "admin"; } public void extendSession(UserSession session) { // Never expire session.expirationTime = new Date(Long.MAX_VALUE); } public void injectPayload(UserSession session) { // Inject malicious data session.sessionData.put("serializedObject", maliciousPayload); }}Proper encapsulation makes these attacks impossible at the language level:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
// ✅ SECURE DESIGN - Encapsulation enforces security policiespublic final class UserSession { private final String sessionId; private final String userId; private final Instant createdAt; private final Instant expiresAt; private final Set<Permission> permissions; private final Map<String, String> sessionAttributes; private volatile boolean invalidated; // Factory method controls creation, generates cryptographic session ID public static UserSession create(User user, Duration validityPeriod) { Objects.requireNonNull(user, "User cannot be null"); Objects.requireNonNull(validityPeriod, "Validity period cannot be null"); if (validityPeriod.isNegative() || validityPeriod.isZero()) { throw new IllegalArgumentException("Validity period must be positive"); } return new UserSession( generateSecureSessionId(), user.getId(), Instant.now(), Instant.now().plus(validityPeriod), Set.copyOf(user.getPermissions()) // Defensive copy ); } // Private constructor - can only be called through factory private UserSession(String sessionId, String userId, Instant createdAt, Instant expiresAt, Set<Permission> permissions) { this.sessionId = sessionId; this.userId = userId; this.createdAt = createdAt; this.expiresAt = expiresAt; this.permissions = permissions; this.sessionAttributes = new ConcurrentHashMap<>(); this.invalidated = false; } // Secure session ID generation - implementation hidden private static String generateSecureSessionId() { byte[] randomBytes = new byte[32]; new SecureRandom().nextBytes(randomBytes); return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes); } // Status checks enforce temporal validity public boolean isValid() { return !invalidated && Instant.now().isBefore(expiresAt); } public boolean hasPermission(Permission permission) { if (!isValid()) { throw new SessionExpiredException("Session is no longer valid"); } return permissions.contains(permission); } public boolean isAdmin() { return hasPermission(Permission.ADMIN); } // Invalidation is one-way - cannot be reversed public void invalidate() { this.invalidated = true; this.sessionAttributes.clear(); } // Session attributes with controlled access public void setAttribute(String key, String value) { if (!isValid()) { throw new SessionExpiredException("Cannot modify expired session"); } validateAttributeKey(key); sessionAttributes.put(key, value); } public Optional<String> getAttribute(String key) { if (!isValid()) { return Optional.empty(); } return Optional.ofNullable(sessionAttributes.get(key)); } private void validateAttributeKey(String key) { if (key == null || key.isEmpty() || key.length() > 100) { throw new IllegalArgumentException("Invalid attribute key"); } // Prevent injection of system attributes if (key.startsWith("__")) { throw new SecurityException("Reserved attribute prefix"); } } // Read-only access to identity public String getSessionId() { return sessionId; } public String getUserId() { return userId; } public Instant getExpiresAt() { return expiresAt; } // Defensive copy of permissions public Set<Permission> getPermissions() { return Set.copyOf(permissions); }}With proper encapsulation: Privilege escalation is impossible — isAdmin is derived from permissions set at creation via a valid User object. Session hijacking is blocked — sessionId is final and generated with cryptographic randomness. Expiration bypass fails — expiresAt is final and checked in isValid(). Data injection is controlled — validateAttributeKey() prevents malicious keys.
Shopping carts demonstrate encapsulation in domain logic—maintaining business invariants around pricing, quantities, and cart operations.
123456789101112131415161718192021222324252627282930313233343536373839404142
// ❌ BUSINESS LOGIC VIOLATIONS - Invariants can be brokenpublic class ShoppingCart { public List<CartItem> items; public double totalPrice; public String discountCode; public double discountPercentage; public ShoppingCart() { items = new ArrayList<>(); totalPrice = 0.0; }} public class CartItem { public String productId; public String productName; public int quantity; public double unitPrice; public double lineTotal;} // Client can violate all business rulesclass FraudulentClient { public void manipulateCart(ShoppingCart cart) { // Set arbitrary discount cart.discountPercentage = 99.9; // 99.9% off! // Modify prices directly for (CartItem item : cart.items) { item.unitPrice = 0.01; // Everything for a penny! item.lineTotal = 0.01; } // Override total cart.totalPrice = 0.01; // Negative quantities? CartItem freeItem = new CartItem(); freeItem.quantity = -100; // Get paid for taking items! cart.items.add(freeItem); }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
// ✅ PROPER ENCAPSULATION - Business rules enforcedpublic final class ShoppingCart { private final String cartId; private final String customerId; private final Map<String, CartItem> items; // productId -> item private DiscountCode appliedDiscount; private final Instant createdAt; public ShoppingCart(String customerId) { this.cartId = UUID.randomUUID().toString(); this.customerId = Objects.requireNonNull(customerId); this.items = new LinkedHashMap<>(); // Preserves insertion order this.appliedDiscount = null; this.createdAt = Instant.now(); } // Adding items enforces validation and auto-calculates public void addItem(Product product, int quantity) { Objects.requireNonNull(product, "Product cannot be null"); if (quantity <= 0) { throw new IllegalArgumentException("Quantity must be positive"); } if (quantity > product.getMaxOrderQuantity()) { throw new QuantityLimitExceededException( "Maximum " + product.getMaxOrderQuantity() + " items allowed" ); } items.compute(product.getId(), (id, existing) -> { if (existing == null) { return new CartItem(product, quantity); } int newQuantity = existing.getQuantity() + quantity; if (newQuantity > product.getMaxOrderQuantity()) { throw new QuantityLimitExceededException( "Cart would exceed maximum quantity" ); } return existing.withQuantity(newQuantity); }); } public void updateQuantity(String productId, int newQuantity) { if (newQuantity <= 0) { removeItem(productId); return; } CartItem item = items.get(productId); if (item == null) { throw new ItemNotFoundException("Product not in cart: " + productId); } if (newQuantity > item.getProduct().getMaxOrderQuantity()) { throw new QuantityLimitExceededException("Maximum quantity exceeded"); } items.put(productId, item.withQuantity(newQuantity)); } public void removeItem(String productId) { if (items.remove(productId) == null) { throw new ItemNotFoundException("Product not in cart: " + productId); } } // Discount application with business rules public void applyDiscountCode(DiscountCode code) { Objects.requireNonNull(code); if (!code.isValid()) { throw new InvalidDiscountException("Discount code has expired"); } if (!code.isApplicableTo(this)) { throw new InvalidDiscountException( "Discount requires minimum spend of " + code.getMinimumSpend() ); } this.appliedDiscount = code; } public void removeDiscount() { this.appliedDiscount = null; } // Calculated properties - no setters, always derived public Money getSubtotal() { return items.values().stream() .map(CartItem::getLineTotal) .reduce(Money.ZERO, Money::add); } public Money getDiscountAmount() { if (appliedDiscount == null) { return Money.ZERO; } return appliedDiscount.calculateDiscount(getSubtotal()); } public Money getTotal() { return getSubtotal().subtract(getDiscountAmount()); } // Read-only access to cart contents public List<CartItemView> getItems() { return items.values().stream() .map(CartItemView::from) // Transform to read-only view objects .collect(Collectors.toUnmodifiableList()); } public int getItemCount() { return items.values().stream() .mapToInt(CartItem::getQuantity) .sum(); } public boolean isEmpty() { return items.isEmpty(); } public String getCartId() { return cartId; } public String getCustomerId() { return customerId; }} // Immutable cart item - internal use onlyfinal class CartItem { private final Product product; private final int quantity; private final Money lineTotal; CartItem(Product product, int quantity) { this.product = product; this.quantity = quantity; this.lineTotal = product.getPrice().multiply(quantity); } CartItem withQuantity(int newQuantity) { return new CartItem(this.product, newQuantity); } // Package-private getters Product getProduct() { return product; } int getQuantity() { return quantity; } Money getLineTotal() { return lineTotal; }} // View object for external consumption - completely immutablepublic record CartItemView( String productId, String productName, int quantity, Money unitPrice, Money lineTotal) { static CartItemView from(CartItem item) { return new CartItemView( item.getProduct().getId(), item.getProduct().getName(), item.getQuantity(), item.getProduct().getPrice(), item.getLineTotal() ); }}Configuration management demonstrates encapsulation for system settings—ensuring validation, type safety, and controlled updates.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
// ✅ ENCAPSULATED CONFIGURATION - Type-safe, validated, immutablepublic final class AppConfiguration { private final DatabaseConfig database; private final CacheConfig cache; private final SecurityConfig security; private final Map<String, String> featureFlags; private final Instant loadedAt; private AppConfiguration(Builder builder) { this.database = Objects.requireNonNull(builder.database); this.cache = Objects.requireNonNull(builder.cache); this.security = Objects.requireNonNull(builder.security); this.featureFlags = Map.copyOf(builder.featureFlags); this.loadedAt = Instant.now(); validate(); } private void validate() { // Cross-cutting validation if (database.getMaxConnections() < cache.getPoolSize()) { throw new ConfigurationException( "Database connections must be >= cache pool size" ); } if (security.getSessionTimeout().compareTo(Duration.ofMinutes(1)) < 0) { throw new ConfigurationException( "Session timeout must be at least 1 minute" ); } } // Type-safe access to nested configurations public DatabaseConfig database() { return database; } public CacheConfig cache() { return cache; } public SecurityConfig security() { return security; } // Feature flag queries with defaults public boolean isFeatureEnabled(String feature) { return "true".equalsIgnoreCase(featureFlags.getOrDefault(feature, "false")); } public Optional<String> getFeatureFlag(String feature) { return Optional.ofNullable(featureFlags.get(feature)); } // Builder pattern for controlled construction public static Builder builder() { return new Builder(); } public static class Builder { private DatabaseConfig database; private CacheConfig cache; private SecurityConfig security; private Map<String, String> featureFlags = new HashMap<>(); public Builder database(DatabaseConfig config) { this.database = config; return this; } public Builder cache(CacheConfig config) { this.cache = config; return this; } public Builder security(SecurityConfig config) { this.security = config; return this; } public Builder featureFlag(String name, String value) { this.featureFlags.put(name, value); return this; } public AppConfiguration build() { return new AppConfiguration(this); } }} // Nested configuration - also immutable and validatedpublic final class DatabaseConfig { private final String host; private final int port; private final String database; private final int maxConnections; private final Duration connectionTimeout; public DatabaseConfig(String host, int port, String database, int maxConnections, Duration connectionTimeout) { this.host = requireNonEmpty(host, "host"); this.port = requireValidPort(port); this.database = requireNonEmpty(database, "database"); this.maxConnections = requirePositive(maxConnections, "maxConnections"); this.connectionTimeout = requirePositiveDuration(connectionTimeout); } private String requireNonEmpty(String value, String name) { if (value == null || value.trim().isEmpty()) { throw new ConfigurationException(name + " cannot be empty"); } return value.trim(); } private int requireValidPort(int port) { if (port < 1 || port > 65535) { throw new ConfigurationException("Port must be 1-65535"); } return port; } private int requirePositive(int value, String name) { if (value <= 0) { throw new ConfigurationException(name + " must be positive"); } return value; } private Duration requirePositiveDuration(Duration duration) { if (duration == null || duration.isNegative() || duration.isZero()) { throw new ConfigurationException("Duration must be positive"); } return duration; } // Immutable access public String getHost() { return host; } public int getPort() { return port; } public String getDatabase() { return database; } public int getMaxConnections() { return maxConnections; } public Duration getConnectionTimeout() { return connectionTimeout; } // Derived connection string - implementation detail public String toConnectionString() { return String.format("jdbc:postgresql://%s:%d/%s", host, port, database); }}This design ensures: Type safety — You can't set a duration where an integer is expected. Validation at load time — Invalid configurations fail fast with clear errors. Immutability after construction — No code can accidentally modify running configuration. Cross-cutting validation — The validate() method checks consistency between related settings.
Across all these examples, certain encapsulation patterns appear repeatedly. Recognizing these patterns will help you apply encapsulation systematically in your own code:
| Pattern | When to Apply | Example from This Page |
|---|---|---|
| Private fields + public methods | Always — this is the foundation of encapsulation | All examples use private fields with controlled accessors |
| Immutable identity fields (final) | When values should never change after construction | BankAccount.accountNumber, UserSession.sessionId |
| Constructor validation | When invalid states should be impossible to create | All examples validate parameters in constructors |
| Factory methods | When construction logic is complex or needs control | UserSession.create() generates secure session IDs |
| Defensive copying | When returning mutable collections or accepting mutable inputs | BankAccount.getTransactionHistory() returns copy |
| Calculated properties (no setter) | When values should be derived, not set directly | ShoppingCart.getTotal() is calculated, never assigned |
| Builder pattern | When many optional parameters or complex configuration | AppConfiguration uses Builder for readable construction |
| View/DTO objects | When exposing data without exposing internal structure | CartItemView provides read-only snapshot of CartItem |
Through these examples, we've seen encapsulation transform from an abstract principle into concrete code practices:
What's next:
Now that we've seen good encapsulation in action, the next page examines the opposite: common encapsulation mistakes that developers make and how to recognize them. Understanding what not to do is just as important as knowing the right approach.
You now have a library of encapsulation examples to draw from. Use these patterns as templates when designing your own classes. Remember: the goal isn't to make code complex, but to make it correct by making incorrect usage impossible.