Kotlin data class, Python @dataclass, Java record, and C# record
Boilerplate code is one of the quiet productivity killers in software engineering. It bloats codebases, obscures intent, increases maintenance cost, and—ironically—introduces bugs in code that “should have been trivial.”
Modern programming languages have converged on a powerful idea: data-centric types should be concise, immutable by default, and behaviorally predictable. Kotlin, Python, Java, and C# all provide first-class constructs to achieve this.
This article explains how to avoid boilerplate code using:
- Kotlin
data class - Python
@dataclass - Java
record - C#
record
We’ll use production-style examples, not toy snippets.
The Problem: Boilerplate in Data Models
Traditional object-oriented data models typically require you to manually implement:
- Constructors
- Getters / setters
equals/hashCodetoString- Copy or cloning logic
Example (classic Java POJO):
public class User {
private final String id;
private final String email;
private final boolean active;
public User(String id, String email, boolean active) {
this.id = id;
this.email = email;
this.active = active;
}
public String getId() { return id; }
public String getEmail() { return email; }
public boolean isActive() { return active; }
// equals, hashCode, toString...
}
This is structural noise. The signal—what the data actually is—is buried.
Kotlin: data class for Expressive Domain Models
Kotlin’s data class is designed for value semantics. The compiler generates:
equals()/hashCode()toString()copy()- Component functions (for destructuring)
Production Example: Order Snapshot
data class OrderSnapshot(
val orderId: String,
val customerId: String,
val totalAmount: BigDecimal,
val currency: Currency,
val createdAt: Instant
)
Why This Matters in Production
val original = OrderSnapshot(
orderId = "ORD-1001",
customerId = "C-42",
totalAmount = BigDecimal("149.99"),
currency = Currency.getInstance("USD"),
createdAt = Instant.now()
)
val refunded = original.copy(totalAmount = BigDecimal.ZERO)
✔ Immutable by default
✔ Safe copying without mutation
✔ Excellent logging and debugging via toString()
✔ Ideal for DTOs, events, API contracts, and aggregates
Best Practice:
Use data class only for pure data. Avoid embedding heavy business logic.
Python: @dataclass for Clean, Explicit Models
Python’s @dataclass decorator removes boilerplate while preserving flexibility. You get:
- Auto-generated
__init__ __repr____eq__- Optional immutability
Production Example: API Payload Model
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
@dataclass(frozen=True)
class PaymentRequest:
transaction_id: str
amount: float
currency: str
requested_at: datetime
description: Optional[str] = None
Usage in Real Systems
request = PaymentRequest(
transaction_id="TXN-9912",
amount=49.99,
currency="EUR",
requested_at=datetime.utcnow()
)
✔ Explicit and readable
✔ Type-hint friendly (works with mypy, pydantic, FastAPI)
✔ frozen=True enables safe immutability
✔ Excellent for microservices and message schemas
Best Practice:
Use @dataclass for data transport and configuration, not as an Active Record replacement.
Java: record for Immutable Value Objects
Java records (Java 16+) finally give Java a first-class value type.
A record automatically provides:
- Canonical constructor
- Accessors
equals,hashCode,toString
Production Example: Audit Log Entry
import java.time.Instant;
public record AuditEvent(
String eventId,
String actor,
String action,
Instant timestamp
) {}
Why Records Are a Big Deal
AuditEvent event = new AuditEvent(
"EVT-7",
"system",
"USER_LOGIN",
Instant.now()
);
✔ Enforced immutability
✔ Clear intent: this is data, not behavior
✔ Perfect for events, projections, DTOs, and query models
✔ Eliminates 70–80% of POJO boilerplate
Best Practice:
Use records for value objects, not entities with lifecycle or identity mutation.
C#: record for Modern, Immutable Data Types
C# records bring value-based equality and non-destructive mutation to .NET.
Production Example: Configuration Snapshot
public record ServiceConfig(
string ServiceName,
string Endpoint,
int TimeoutSeconds,
bool EnableRetries
);
Practical Usage
var defaultConfig = new ServiceConfig(
"BillingService",
"https://billing.internal",
30,
true
);
var noRetryConfig = defaultConfig with { EnableRetries = false };
✔ Value-based equality
✔ Immutability by default
✔ Safe, expressive copying using with
✔ Ideal for configuration, DTOs, and messages
Best Practice:
Prefer record over class when the primary concern is data correctness, not identity.
Cross-Language Comparison
| Language | Feature | Immutability | Copy Support | Ideal Use Case |
|---|---|---|---|---|
| Kotlin | data class | By convention | copy() | Domain models, DTOs |
| Python | @dataclass | Optional | Manual / replace | API schemas, configs |
| Java | record | Enforced | New instance | Value objects, events |
| C# | record | Default | with | DTOs, snapshots |
Final Thoughts: Design for Intent, Not Ceremony
Boilerplate is not just annoying—it’s a design smell. Modern data constructs allow you to:
- Express intent clearly
- Reduce maintenance risk
- Improve correctness and readability
- Align with immutability and functional principles
If a type primarily holds data, let the language work for you.
Less boilerplate.
More intent.
Better software.

