Loading learning content...
Choosing the right attributes for a class is more art than science. Every domain concept has infinite potential properties you could capture—the challenge is deciding which ones you should. A Person class could track name, age, height, weight, eye color, favorite food, shoe size, childhood pets, and countless other details. But which attributes actually serve your application's needs?
Meaningful attributes are those that capture the essential characteristics of an entity within the context of your system. They're neither too few (leaving the class unable to fulfill its responsibilities) nor too many (cluttering the class with irrelevant data).
By the end of this page, you will know how to identify essential attributes for your classes, apply principles for choosing appropriate types and granularity, avoid common attribute design mistakes, and create classes that accurately model your domain.
The attributes a class needs depend entirely on the context in which it exists. The same real-world entity modeled for different purposes requires different attributes.
Example: The "Person" Entity
Consider how a Person would be modeled in different systems:
Each context demands different attributes because each system cares about different aspects of a person. There's no universal "correct" set of person attributes—correctness is defined by purpose.
When selecting attributes, ask: 'What does this system need to know about this entity to fulfill its responsibilities?' Any attribute that doesn't contribute to answering this question is probably unnecessary.
The Principle of Essential Properties:
Attributes should capture essential rather than incidental properties:
Essential properties define what the entity is in your context. An Order without items isn't really an order. An Employee without an ID can't function in an HR system.
Incidental properties are nice-to-have but not fundamental. An employee's favorite color might be interesting but irrelevant to HR operations.
This distinction isn't absolute—what's incidental in one system is essential in another. A social network cares about favorite colors; an HR system doesn't.
How do you decide when a concept should be a single attribute versus multiple attributes versus a separate class? This question of granularity affects maintainability and flexibility.
The Name Example:
Should a person's name be:
String name?firstName and lastName?Name object with prefix, firstName, middleName, lastName, suffix?The answer depends on your needs:
| Consideration | Favor Single Attribute | Favor Multiple/Structured |
|---|---|---|
| Operations needed | Only store and display | Parse, sort, search, format differently |
| Validation complexity | Simple validation | Different rules for each part |
| Data variations | Uniform format | Multiple formats or optional parts |
| Future flexibility | Unlikely to change | May evolve with new requirements |
| Reusability | Unique to this class | Same structure appears elsewhere |
123456789101112131415161718192021222324252627282930313233
// COARSE GRANULARITY: Simple but inflexiblepublic class Employee { private String address; // "123 Main St, Springfield, IL 62701"} // FINE GRANULARITY: Flexible but verbosepublic class Employee { private String streetNumber; private String streetName; private String city; private String state; private String zipCode; private String country;} // APPROPRIATE GRANULARITY: Structured as a reusable objectpublic class Employee { private Address address; // Encapsulates address complexity} public class Address { private String streetLine1; private String streetLine2; private String city; private String state; private String postalCode; private String country; // Address can validate itself, format itself, compare itself public String formatForMailing() { ... } public boolean isInternational() { ... } public boolean isValid() { ... }}A strong signal for extracting attributes into a separate class is when you find yourself writing methods that operate only on those attributes. If you're writing validateAddress(), formatAddress(), parseAddress()—the address concept deserves its own class.
The type you choose for an attribute communicates intent and enforces constraints. Thoughtful type selection prevents entire categories of bugs.
Principle: Use the Most Specific Type That Fits
Rather than using generic types, prefer types that express constraints:
String for email → use an EmailAddress value objectint for quantity → use a constrained Quantity or PositiveIntString for status → use an enum OrderStatuslong for money → use a Money or BigDecimal with currencyString for phone → use a PhoneNumber value object123456789101112131415161718192021222324252627282930313233
// VALUE OBJECT: Encapsulates validation and semanticspublic final class EmailAddress { private final String value; public EmailAddress(String value) { if (value == null || !isValidEmail(value)) { throw new IllegalArgumentException("Invalid email: " + value); } this.value = value.toLowerCase().trim(); // Normalize } private static boolean isValidEmail(String email) { // Real validation logic return email.contains("@") && email.contains("."); } public String getValue() { return value; } public String getDomain() { return value.substring(value.indexOf('@') + 1); } // Equals, hashCode, toString...} // USAGE: Impossible to have invalid email in the systempublic class User { private EmailAddress email; // Can never be null or invalid public void setEmail(EmailAddress email) { this.email = Objects.requireNonNull(email); }}Primitive obsession—using primitives for domain concepts—is a pervasive code smell. Every time you validate a string parameter for format, consider: should this be a type that validates on construction? If yes, create a value object.
Not every piece of data is always present. Some attributes are optional—they might have a value, or they might not. Handling optionality explicitly prevents null-related bugs.
The Problem with Nullable Attributes:
When an attribute can be null, every piece of code that accesses it must check for null. Forget a check, get a NullPointerException. The type system doesn't help—String and String-that-might-be-null have the same type.
Solutions for Optional Attributes:
1234567891011121314151617181920212223242526272829303132333435363738394041
public class CustomerProfile { // REQUIRED: Must always have a value private final String customerId; private final String email; // OPTIONAL: May or may not have a value private final String phoneNumber; // Could be null private final LocalDate dateOfBirth; // Could be null // OPTION 1: Return Optional from getters public Optional<String> getPhoneNumber() { return Optional.ofNullable(phoneNumber); } public Optional<LocalDate> getDateOfBirth() { return Optional.ofNullable(dateOfBirth); } // OPTION 2: Provide null-safe utility methods public boolean hasPhoneNumber() { return phoneNumber != null; } public String getPhoneNumberOrDefault(String defaultValue) { return phoneNumber != null ? phoneNumber : defaultValue; } // OPTION 3: For collections, never null - empty instead private List<Address> addresses = new ArrayList<>(); // Never null public List<Address> getAddresses() { return Collections.unmodifiableList(addresses); // Never null }} // CALLER CODE: Clear handling of optionalityprofile.getPhoneNumber() .ifPresent(phone -> sendSMS(phone, message)); String phone = profile.getPhoneNumber() .orElse("Not provided");A null collection forces callers to check for null before every operation. Return empty collections instead. There's no difference between 'no items' and 'null list' semantically—both mean zero items. But empty lists are safe to iterate; null lists throw exceptions.
Well-named attributes are self-documenting. Poor names obscure meaning and invite bugs.
Naming Principles:
deliveryAddress over address when there are multiplecustomerId not custId or cid| Poor Name | Better Name | Why It's Better |
|---|---|---|
| data | customerProfile | Specific, indicates content |
| flag | isActive | Boolean intent clear |
| temp | temperatureCelsius | Includes unit, clear purpose |
| list1 | pendingOrders | Describes what's in the list |
| dt | createdAt | Clear meaning, standard pattern |
| x | xCoordinate | Full word, clear context |
| mgr | reportingManager | Unabbreviated, relationship clear |
Special Naming Patterns:
Boolean attributes should read as assertions:
isActive, hasChildren, canEdit, shouldNotifyisValid not isNotInvalidCollections should be plural:
orders, items, addressesorderList (redundant), orders (better)Timestamps should indicate the event:
createdAt, updatedAt, deletedAt, lastLoginAtReferences to other objects should indicate relationship:
parentCategory, reportingManager, primaryAddressUse the same terminology your domain experts use. If the business calls it a 'fulfillment center,' don't code it as 'warehouse.' Consistent language between code and domain reduces translation errors and improves communication.
Some values can be either stored explicitly or computed from other data. Choosing correctly affects consistency, performance, and maintainability.
Stored Attributes:
Derived Attributes:
12345678910111213141516171819202122232425262728293031323334353637383940414243
public class Order { private List<OrderItem> items = new ArrayList<>(); // DERIVED: Computed from source data // Always accurate, never stale, but computed each time public double getTotal() { return items.stream() .mapToDouble(OrderItem::getSubtotal) .sum(); } public int getItemCount() { return items.size(); } // Questions for each potential derived attribute: // 1. How expensive is the computation? // 2. How often is the value accessed? // 3. How often does source data change? // 4. How critical is it that the value is current?} // WHEN TO STORE INSTEAD:public class HighVolumeOrder { private List<OrderItem> items = new ArrayList<>(); private double cachedTotal; // Stored for performance // Update cached value when items change public void addItem(OrderItem item) { items.add(item); cachedTotal += item.getSubtotal(); // Maintain cache } public void removeItem(OrderItem item) { if (items.remove(item)) { cachedTotal -= item.getSubtotal(); // Maintain cache } } public double getTotal() { return cachedTotal; // O(1) access }}Start with derived attributes. Only add caching (stored values) when profiling shows a performance problem. Premature caching adds complexity and synchronization bugs. The simplest correct solution is best until proven otherwise.
Experienced developers recognize these anti-patterns. Avoiding them leads to cleaner, more maintainable classes.
1234567891011121314151617181920212223242526272829303132333435
// BEFORE: Flag explosion, implicit coupling, stringly-typedpublic class User { private String status; // "active", "inactive", "suspended", "deleted" private boolean isVerified; private boolean hasCompletedOnboarding; private boolean is2FAEnabled; private String addressLine1; private String addressLine2; private String city; private String state; private String zipCode; private String country;} // AFTER: Enums, grouped attributes, clear structurepublic class User { private UserStatus status; // Enum: ACTIVE, INACTIVE, SUSPENDED, DELETED private UserFlags flags; // Value object: verified, onboarded, 2fa private Address address; // Value object: encapsulates address fields} public enum UserStatus { ACTIVE, INACTIVE, SUSPENDED, DELETED} public class UserFlags { private final boolean isVerified; private final boolean hasCompletedOnboarding; private final boolean is2FAEnabled; // Meaningful methods public boolean isFullyOnboarded() { return isVerified && hasCompletedOnboarding; }}We've explored the principles and practices of attribute design. Let's consolidate:
What's Next:
We've covered how to design meaningful attributes. The final page in this module addresses the flip side: designing cohesive methods—methods that work together harmoniously, each with a clear purpose, creating classes that are focused and maintainable.
You now understand how to select and design attributes that serve your domain effectively. You can choose appropriate types and granularity, handle optionality, name attributes meaningfully, and avoid common pitfalls. Next, we'll design cohesive methods.