Loading content...
Having established why accessors exist, we now face a practical question: When should you provide a getter? The answer is not as simple as "always" or "never"—it requires careful consideration of what information your object genuinely needs to share with the outside world.
Getters are often created reflexively—developers define a private field and immediately generate a getter, treating it as a mandatory pairing. This habit undermines encapsulation by exposing implementation details that should remain hidden.
This page teaches you to think critically about each getter: Is this information part of the object's public contract? Does exposing it serve the object's clients, or does it merely leak implementation details? The goal is to expose exactly what's needed—no more, no less.
By the end of this page, you will understand the legitimate use cases for getters, how to distinguish between appropriate exposure and information leakage, and how to design getters that serve your object's clients without compromising encapsulation. You'll develop a decision framework for determining when a getter is warranted.
Before creating any getter, answer these questions:
1. Is this information part of the object's public identity?
Some information is intrinsic to what an object represents. A Person object naturally exposes a name. A Rectangle naturally exposes width and height. These are part of the object's conceptual identity, not implementation details.
2. Do clients genuinely need this information?
The key word is need. If clients need to display, compare, or base decisions on this value, a getter is warranted. If you're exposing information "just in case," you're leaking implementation details.
3. Can the object perform the operation instead?
Often, when clients request data, what they really want is for the object to do something with that data. Instead of providing a getter so clients can perform a calculation, consider whether the object should perform that calculation itself.
4. Is the representation stable?
If the internal representation might change significantly, exposing it through a getter locks you into that representation or forces complex conversions later.
| Question | If Yes | If No |
|---|---|---|
| Is it part of public identity? | Strong candidate for getter | Probably shouldn't expose |
| Do clients genuinely need it? | Consider providing getter | Don't create getter |
| Can object perform operation instead? | Provide behavior, not data | Getter may be appropriate |
| Is representation stable? | Safe to expose through getter | Consider abstracting or omitting |
Start with no getters. Add each one deliberately when you have a concrete use case. This is the opposite of the typical approach (generate all getters, maybe remove some later), and it leads to much tighter encapsulation.
Let's examine the scenarios where getters are genuinely appropriate and enhance rather than undermine your design.
Person.getName(), Order.getId(), Product.getSku()123456789101112131415161718192021222324252627
// Identity attributes - appropriate for getterspublic class Employee { private final String employeeId; private final String name; private final String department; private LocalDate hireDate; // Identity getter - this IS the employee's identity public String getEmployeeId() { return employeeId; } // Core attribute - clients need this for display public String getName() { return name; } // Core attribute - used for organizational queries public String getDepartment() { return department; } // Core attribute - needed for HR calculations public LocalDate getHireDate() { return hireDate; }}1234567891011121314151617181920212223242526272829303132333435363738394041424344
// Presentation-focused getterspublic class Order { private String orderId; private LocalDateTime createdAt; private List<OrderLine> lines; private Money subtotal; private Money tax; private Money total; // Presentation: Display order reference public String getOrderId() { return orderId; } // Presentation: Show when order was placed public LocalDateTime getCreatedAt() { return createdAt; } // Presentation: Display price breakdown public Money getSubtotal() { return subtotal; } public Money getTax() { return tax; } public Money getTotal() { return total; } // Presentation: List items in UI public List<OrderLine> getLines() { return Collections.unmodifiableList(lines); } // Computed presentation value public int getItemCount() { return lines.stream() .mapToInt(OrderLine::getQuantity) .sum(); }}123456789101112131415161718192021222324
// Decision support - external code needs these valuespublic class Task { private String title; private Priority priority; private LocalDateTime dueDate; private TaskStatus status; // Getters for external decision-making public Priority getPriority() { return priority; } public LocalDateTime getDueDate() { return dueDate; } public TaskStatus getStatus() { return status; }} // External code legitimately uses these for sorting/filteringList<Task> urgentTasks = tasks.stream() .filter(t -> t.getPriority() == Priority.HIGH) .filter(t -> t.getDueDate().isBefore(tomorrow)) .sorted(comparing(Task::getDueDate)) .collect(toList()); // Configuration system inspects propertiesif (task.getStatus() == TaskStatus.BLOCKED) { notificationService.alertTeamLead(task);}Circle.getArea(), Person.getAge(), Order.isOverdue()1234567891011121314151617181920212223242526272829303132333435
public class Subscription { private LocalDate startDate; private SubscriptionPlan plan; // Computed: no field for 'expirationDate' public LocalDate getExpirationDate() { return startDate.plus(plan.getDuration()); } // Computed: no field for 'isActive' public boolean isActive() { return LocalDate.now().isBefore(getExpirationDate()); } // Computed: no field for 'daysRemaining' public long getDaysRemaining() { return ChronoUnit.DAYS.between( LocalDate.now(), getExpirationDate() ); } // Computed: no field for 'percentage used' public double getUsagePercentage() { long total = ChronoUnit.DAYS.between( startDate, getExpirationDate() ); long elapsed = ChronoUnit.DAYS.between( startDate, LocalDate.now() ); return (double) elapsed / total * 100; }}equals(), hashCode(), and compareTo() requires field accessComparator could be provided instead of raw gettersNot all getters are created equal. Some patterns suggest that a getter is actually a design problem in disguise.
a.getB().getC().getD(), encapsulation is breaking down.get in the chain increases coupling to internal structureB's internal representation changes, all callers break1234567891011121314151617181920
// ❌ BAD: Getter chain exposes deep structuredouble salary = employee .getDepartment() .getManager() .getCompensation() .getBaseSalary(); // If any intermediate class changes structure, this breaks// The caller now knows: Employee has Department,// Department has Manager, Manager has Compensation, etc. // ✅ BETTER: Ask the object to perform the operation// Employee knows how to find relevant salary informationOptional<Double> managerSalary = employee.getManagerSalary(); // Or provide a focused interfaceinterface SalaryService { Optional<Money> getManagerSalaryFor(Employee employee);}if (obj.getStatus() == X) { doA(); } else if (obj.getStatus() == Y) { doB(); }123456789101112131415161718192021222324252627282930
// ❌ BAD: Getter + conditional logic scattered in callerspublic class PaymentProcessor { public void process(Payment payment) { if (payment.getStatus() == PaymentStatus.PENDING) { processNewPayment(payment); } else if (payment.getStatus() == PaymentStatus.AUTHORIZED) { capturePayment(payment); } else if (payment.getStatus() == PaymentStatus.FAILED) { retryPayment(payment); } // This logic is duplicated everywhere payments are handled }} // ✅ BETTER: Payment owns its behaviorpublic class Payment { private PaymentStatus status; private PaymentStrategy strategy; public void process(PaymentProcessor processor) { // Object knows its own behavior strategy.process(this, processor); } // Or use polymorphism through state pattern public void advance() { this.status = status.next(this); }}getInternalMap(), getCache()getValidator(), getSerializer()getConnectionString(), getBufferSize()1234567891011121314151617181920212223242526272829303132333435
// ❌ BAD: Exposing internal infrastructurepublic class UserRepository { private HikariDataSource dataSource; private Map<Long, User> cache; private UserValidator validator; // These expose implementation details! public DataSource getDataSource() { return dataSource; } public Map<Long, User> getCache() { return cache; } public UserValidator getValidator() { return validator; }} // ✅ BETTER: Hide infrastructure behind domain operationspublic class UserRepository { private HikariDataSource dataSource; private Map<Long, User> cache; private UserValidator validator; // Domain operations - infrastructure is hidden public Optional<User> findById(Long id) { return Optional.ofNullable(cache.get(id)) .or(() -> loadFromDatabase(id)); } public void save(User user) { validator.validate(user); saveToDatabase(user); cache.put(user.getId(), user); } // If monitoring is needed, provide focused methods public int getCacheSize() { return cache.size(); } public HealthStatus getHealth() { /* check dataSource */ }}12345678910111213141516171819202122232425262728293031323334
// ❌ DANGEROUS: Returning mutable collectionpublic class ShoppingCart { private List<CartItem> items = new ArrayList<>(); public List<CartItem> getItems() { return items; // Caller can do anything! }} // External code breaks all invariants:cart.getItems().clear(); // Empty the cart without proper handlingcart.getItems().add(null); // Add invalid datacart.getItems().set(0, new CartItem(freeProduct, 1000)); // Steal! // ✅ SAFE: Return unmodifiable viewpublic class ShoppingCart { private List<CartItem> items = new ArrayList<>(); public List<CartItem> getItems() { return Collections.unmodifiableList(items); } // All modifications go through controlled methods public void addItem(Product product, int quantity) { // Validate, update totals, fire events, etc. }} // ✅ ALTERNATIVE: Return a stream for iterationpublic Stream<CartItem> items() { return items.stream();}When you've determined that a getter is warranted, follow these principles to design it well:
getAge() over getBirthDateField() even if age is computed from birthDateCollections.unmodifiableList(list) or new ArrayList<>(list)LocalDate is immutable (safe), but Date is mutable (copy it!)12345678910111213141516171819202122232425262728
public class Event { private final String name; private final Date legacyStartDate; // Mutable old API private final LocalDateTime modernStartDate; // Immutable new API private final List<Attendee> attendees; // Safe: String is immutable public String getName() { return name; } // Safe: LocalDateTime is immutable public LocalDateTime getModernStartDate() { return modernStartDate; } // Defensive copy: Date is mutable! public Date getLegacyStartDate() { return new Date(legacyStartDate.getTime()); } // Unmodifiable view: prevents external modification public List<Attendee> getAttendees() { return Collections.unmodifiableList(attendees); } // For deep hierarchies, consider deep copies or immutable wrappers}Optional<T> for nullable values, not bare T12345678910111213141516171819202122232425262728293031323334353637383940
public class Customer { private String customerId; // Always present private String email; // Required private String phoneNumber; // Optional private Address shippingAddress; // Optional private String nickName; // Optional, has default // Clear: never null public String getCustomerId() { return customerId; } // Clear: never null public String getEmail() { return email; } // Explicit: might be absent public Optional<String> getPhoneNumber() { return Optional.ofNullable(phoneNumber); } // Explicit: might be absent public Optional<Address> getShippingAddress() { return Optional.ofNullable(shippingAddress); } // Alternative: provide default, never null public String getDisplayName() { return nickName != null ? nickName : email.split("@")[0]; }} // Usage is now self-documentingcustomer.getPhoneNumber() .ifPresent(phone -> sendSMS(phone, message)); String label = customer.getShippingAddress() .map(Address::getLabel) .orElse("No shipping address");calculate...() or compute...() signal possible expense12345678910111213141516171819202122232425262728293031
public class Report { private List<Transaction> transactions; private Money cachedTotal; private boolean totalValid; // Simple getter - always fast public int getTransactionCount() { return transactions.size(); // O(1) } // Computed but cached - fast after first call public Money getTotal() { if (!totalValid) { cachedTotal = computeTotal(); totalValid = true; } return cachedTotal; } // Name signals this might be slow public DetailedBreakdown calculateDetailedBreakdown() { // Expensive computation - name makes this clear return analyzeTransactions(transactions); } // Invalidate cache when data changes public void addTransaction(Transaction t) { transactions.add(t); totalValid = false; // Invalidate cache }}getX() in Java, x property in C#/Kotlin/PythonisActive(), hasPermission(), canEdit() read naturallygetIsActive() (redundant)getItems(), getUsers()Before adding any getter, run it through this checklist. Each "no" answer is a reason to reconsider whether the getter should exist.
If you can't clearly justify a getter, don't create it. You can always add one later when a genuine need arises. Removing a getter from a public API, however, is a breaking change. Start conservative.
Often, when you're tempted to add a getter, there's a better design that keeps behavior inside the object. Here are common alternatives:
cart.getTotal() so you can if (total > limit), ask cart.exceedsLimit(limit)user.getRole() for permission checks, ask user.canPerform(action)12345678910111213
// ❌ Getter forces caller to implement logicMoney total = cart.getTotal();if (total.isGreaterThan(spendingLimit)) { throw new SpendingLimitExceededException();} // ✅ Object owns the behaviorif (cart.exceedsSpendingLimit(spendingLimit)) { throw new SpendingLimitExceededException();} // Or even better - object handles the consequencecart.validateAgainstSpendingLimit(spendingLimit); // Throws if exceededtoDto() method that returns exactly what's neededtoString() or format() methodComparator rather than exposing sortable fields123456789101112131415161718
// ❌ Exposing fields for sortingpublic String getLastName() { return lastName; }public String getFirstName() { return firstName; } // Caller must know how to sortusers.sort((a, b) -> { int last = a.getLastName().compareTo(b.getLastName()); return last != 0 ? last : a.getFirstName().compareTo(b.getFirstName());}); // ✅ Provide the comparatorpublic static final Comparator<User> BY_NAME = Comparator.comparing(User::lastName) .thenComparing(User::firstName); // Caller just uses itusers.sort(User.BY_NAME);1234567891011121314151617181920212223242526
// ❌ Exposing data for external processingpublic String getCardNumber() { return cardNumber; }public String getExpiry() { return expiry; }public String getCvv() { return cvv; } // External code processes sensitive data directly (unsafe!) // ✅ Accept a processor that handles the datapublic interface PaymentProcessor { void process(String cardNumber, String expiry, String cvv);} public void processPayment(PaymentProcessor processor) { // Object controls how data is shared processor.process( maskCardNumber(cardNumber), // Can mask or transform expiry, cvv );} // Even better for security: never expose CVV at allpublic PaymentToken tokenize(TokenizationService service) { return service.tokenize(this); // Service has secure access}We've examined when getters are appropriate and when they indicate a design problem. Here are the key takeaways:
What's Next:
We've explored when getters are appropriate. The next page tackles the more controversial topic: when to avoid setters. Setters are often more problematic than getters, and understanding when to omit them is key to building truly encapsulated objects.
You now have a decision framework for when to provide getters. The key insight is that each getter should be a deliberate choice that serves the object's clients, not a reflexive pairing with every private field. Next, we'll explore why setters are often the bigger encapsulation problem.