Utility Methods in Data Models: A Design Dilemma
Adding utility methods to data models can simplify code but introduces potential coupling issues. This article examines the design considerations and common practices.

Developers often face the decision of where to place utility methods that operate on data models. A common scenario arises when a data class, such as a User object in an Android application, contains properties like time formatting preferences (e.g., is24Hour, timezone). These properties are frequently used by various utility functions to format dates and times. The temptation is to embed these utility methods directly within the data model itself, especially if the data model is readily available throughout the application.
Consider a User data class in Kotlin used across multiple screens in an Android app: data class User(val id: String, val name: String, val is24Hour: Boolean, val timezone: ZoneId). Separate utility functions in a DateTimeUtil object handle tasks like getting the current time in a user's timezone or formatting times. Repeatedly passing user-specific formatting preferences to these utilities can become verbose. The proposed solution is to add methods like fun userTzNow(): ZonedDateTime and fun displayTime(time: ZonedDateTime): String directly to the User class, leveraging its timezone and is24Hour properties.
This approach, while seemingly convenient, raises questions about software design principles and best practices. It prompts a deeper look into the separation of concerns, the nature of data models versus behavior-rich objects, and the potential for introducing unwanted coupling. The decision impacts code maintainability, testability, and overall architectural clarity, especially as applications grow in complexity.
The Case for Co-location: Discoverability and Simplicity
Proponents of embedding utility methods within data models argue that it enhances discoverability and simplifies code. When methods are intrinsically tied to the data they operate on, they logically belong together. Imagine needing to format a user's current time; having a user.getCurrentFormattedTime() method directly on the User object is intuitive. This co-location means developers don't need to hunt through separate utility classes or guess where related functionality might reside. For instance, if the User object already carries the necessary context (like timezone and hour format), encapsulating the formatting logic within it means that context doesn't need to be passed around repeatedly.
In a Kotlin Android project, where data classes are prevalent, this pattern can lead to more concise and readable code. Developers consuming the User object can directly invoke its methods without needing to import or instantiate separate utility classes. This is particularly appealing when the data model is central to the application's state, as is often the case with logged-in user information. The simplicity gained can speed up development, especially for features that are heavily dependent on user-specific data transformations.
Concerns About Logic and Data Separation
Conversely, a significant argument against this pattern centers on the principle of separation of concerns. Data models, ideally, should represent pure data structures. Introducing complex business logic or UI-related formatting directly into them can blur this distinction. If a User model is intended for serialization or data persistence, adding methods that deal with display logic (like time formatting) pollutes its core responsibility. This can lead to a situation where a data model, meant to be a simple representation of user data, becomes bloated with disparate functionalities.
The primary concern is that data models should remain agnostic to how their data is presented or used. If the User object is shared across different parts of the application, some of which might not require time formatting, these extra methods become unnecessary baggage. This can lead to a larger, more complex class than is strictly needed for its data-holding purpose. A common sentiment is that data models should be concerned with data, not with presentation logic or complex computations, adhering to a purer object-oriented or data-centric design.
Coupling and Recompilation Implications
Embedding utility methods within a data model can create tighter coupling between the model and the logic it encapsulates. If the formatting logic changes, and this logic resides directly within the User class, any part of the application that uses the User class might need to be recompiled. In larger projects, this can cascade into significant build times and complex dependency management. Keeping logic in separate utility classes, while potentially less discoverable initially, offers better modularity.
This separation means that changes to the formatting utilities can be made and tested independently of the data model itself. If the User model's primary role is to hold user attributes that might be serialized or passed between services, it should ideally remain free from logic that is specific to how that data is displayed on a particular screen or in a specific UI component. This approach aligns with typical design patterns where distinct layers or modules handle data representation, business logic, and UI presentation.
ViewModel vs. Data Model: A Contextual Distinction
The suitability of placing utility methods within a data model often depends on the model's role. If the 'data model' in question is actually a ViewModel in the context of Android architecture components, then including display logic is more acceptable. ViewModels are designed to hold UI-related data and logic, acting as an intermediary between the UI and the rest of the application. In this case, a UserViewModel could legitimately contain methods for formatting user data for display.
However, if the User object is a pure data transfer object (DTO) or a plain data class intended for serialization and broader data representation across the application, then embedding UI-specific logic is generally discouraged. Source material suggests that if the model is a data model for the User, UI concerns should not be added. This is because a pure data model is often reused across multiple UI components, and adding component-specific logic would lead to many unused methods within the model.
Where This Approach Does Not Fit
The practice of including utility methods as properties in data models is generally ill-suited for pure data structures or entities that are intended to be domain models or data transfer objects (DTOs) used across various application layers and services. If the User object needs to be sent to a backend API for persistence or retrieved from a remote service, its primary role should be data representation. Adding display formatting or time zone-aware calculations directly to such an object can create a dependency on UI-specific context that may not be available or relevant in those contexts.
Furthermore, this approach can become problematic in complex applications where the data model is deeply integrated with various components. For instance, if different parts of the application require different formatting rules or utilize the User object in ways that don't involve time display, the embedded methods could become dead code or lead to confusion. It's crucial to ensure that methods added to a data model genuinely enhance its core responsibility and are not simply a convenience for a specific use case that could be handled more cleanly in a dedicated service or presentation layer, such as a custom Formatter class or a UserPresenter.
This article draws on Is it good practice to include utility methods as properties in data models? and answers to it (1, 2), on the Stack Exchange network, licensed under CC BY-SA 4.0.
- #android
- #data models
- #design
- #design-patterns
- #kotlin
- #utility methods


Comments