Meaning is externalized
A raw string does not know whether it is a valid order ID, country code, or payment state.
Primitive obsession is one of the most widespread anti-patterns because it feels efficient at first. Instead of creating types for money, email, status, country, or order identifiers, teams keep using strings, numbers, and booleans. The problem is that primitives are cheap containers but poor guardians of meaning.
In Java order systems, status codes, currency values, payment methods, addresses, and identifiers often start as raw strings. That pushes validation, normalization, and business meaning outward into services, utilities, and controllers, which multiplies duplication and weakens the model.
Primitive obsession usually points to missing value objects, enums, or small domain types. The issue is not that primitives are evil. The issue is that important concepts become less safe and less readable when they are represented only as general-purpose scalar values.
For Java developers, this is especially important because strong type design is one of the easiest ways to improve correctness and readability at the same time. A small type like OrderId, Money, or EmailAddress often removes entire categories of repetition and confusion.
The best refactorings here are often incremental. Start with the most repeated or most error-prone raw concepts and give them clearer representation.
A raw string does not know whether it is a valid order ID, country code, or payment state.
When the value cannot defend itself, every caller must remember the rules separately.
Different concepts become harder to distinguish because they all look like String or int.
package org.javaomnibus.ecommerce.antipatterns;
public record PaymentRequest(
String orderId,
String customerEmail,
String currency,
double amount,
String paymentMethod,
String status
) {}
Too many tiny types introduced without clear value can annoy readers. The best candidates are concepts that carry repeated rules, high business meaning, or frequent misuse risk.