Definition
- Entity – a JPA-managed class annotated with
@Entitythat maps to a database table, is tracked by the persistence context, and should never be exposed directly through the API. - DTO (Data Transfer Object) – a plain class, often a Java
record, that carries data across layers or the API boundary, typically with@NotBlank/@DecimalMinvalidation annotations but no business logic. - POJO (Plain Old Java Object) – any Java object free of framework coupling that may hold business logic, such as a
Moneyvalue object or a domain model with rules in its methods. - Layer rule – entities are for persistence, DTOs are for transport, and POJOs are for framework-free domain modelling.
Think of three documents at National Bank of India. The Entity is the official ledger record in the vault, managed under strict rules, tied to a specific database row, and never handed to a customer. The DTO is the counter slip the clerk fills to pass information to or from Rajesh, with checkboxes for validation. The POJO is a scratch worksheet an analyst uses to calculate interest, following the bank's own rules, not tied to any system, just plain logic on paper.
Understanding the Concepts
What is a JPA Entity?
An Entity is annotated with @Entity and mapped to a database table. It is managed by the JPA persistence context, meaning changes to its fields inside an open transaction are automatically flushed to the database. It has an identity column, often relationships to other entities, and lifecycle callbacks.
Because it is tied to the schema and the persistence context, you keep it internal. Exposing it risks leaking columns, coupling the API to the schema, and triggering LazyInitializationException when a serializer touches an unloaded relationship outside a transaction.
package com.bank.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.util.List;
@Entity
@Table(name = "accounts")
public class Account {
@Id private String id;
private String holderName;
@Version private long version; // optimistic locking
private String passwordHash; // never expose
private int internalRiskScore; // never expose
@OneToMany(mappedBy = "account", fetch = FetchType.LAZY)
private List<Transaction> transactions; // lazy-loaded
// getters and setters
}
What is a DTO?
A DTO is a plain class, frequently a Java record for immutability, whose purpose is to move data across a boundary. It is not managed by JPA and has no database identity. It commonly carries validation annotations for request shapes and defines the exact fields exposed to clients. It holds no business logic; it just carries data.
package com.bank.dto;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
public record TransferRequest(
@NotBlank String fromAccount,
@NotBlank String toAccount,
@DecimalMin("0.01") BigDecimal amount
) {}
// Validation annotations, no logic, immutable
What is a POJO?
POJO stands for Plain Old Java Object. It is the broadest term: any Java object not tied to a framework. An Entity is technically a POJO with JPA annotations, and a DTO is a POJO used for transport, but when people say POJO specifically, they mean a framework-free domain object that may contain business logic.
package com.bank.domain;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class Money {
private final BigDecimal amount;
private final String currency;
public Money(BigDecimal amount, String currency) {
if (amount.scale() > 2) {
throw new IllegalArgumentException("Max 2 decimal places for money");
}
this.amount = amount;
this.currency = currency;
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add different currencies");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money applyInterest(BigDecimal rate) {
BigDecimal interest = amount.multiply(rate).setScale(2, RoundingMode.HALF_UP);
return new Money(amount.add(interest), currency);
}
}
This Money class has real business logic but zero framework coupling, which makes it easy to unit test and reuse anywhere.
How are Entity, DTO, and POJO different? (comparison table)
| Feature | Entity | DTO | POJO |
|---|---|---|---|
| Framework coupling | JPA (@Entity, @Id) | Validation (@NotBlank) | None |
| Business logic | No (persistence only) | No (transport only) | Yes |
| Database identity | Yes (@Id) | No | No |
| Mutability | Mutable (managed) | Typically immutable (record) | Either |
| Where it lives | Persistence layer | API boundary / between layers | Domain layer |
| Exposed to clients | Never | Always | Rarely |
When do you use each?
The flow in a typical request is: the controller receives a request DTO, the service maps it toward the domain, uses entities for persistence through the repository, performs logic possibly using POJO value objects, then maps an entity to a response DTO to return. The entity never escapes the service boundary.
package com.bank.service;
import com.bank.dto.*;
import com.bank.domain.Money;
import com.bank.model.Account;
import java.math.BigDecimal;
public class TransferService {
public TransferResponse transfer(TransferRequest req) {
Account from = repository.findById(req.fromAccount()).orElseThrow();
Account to = repository.findById(req.toAccount()).orElseThrow();
// POJO value object for business logic:
Money amount = new Money(req.amount(), "INR");
// Entity – persistence concern:
from.debit(amount.getAmount());
to.credit(amount.getAmount());
repository.save(from);
repository.save(to);
// DTO – transport concern:
return new TransferResponse("TXF-001", "COMPLETED", from.getBalance());
}
}
What are common mistakes?
The biggest mistake is collapsing all three into the entity: adding @NotBlank validation annotations to the entity, adding @JsonIgnore for serialization, and returning it directly from the controller. This couples persistence, transport, and validation into one class.
// Wrong – entity doing all three jobs:
@Entity
public class Account {
@Id private String id;
@NotBlank private String holderName; // transport validation on entity
@JsonIgnore private String passwordHash; // serialization concern on entity
private BigDecimal balance;
}
// Correct – separate responsibilities:
@Entity class Account { /* persistence only */ }
record CreateAccountRequest(@NotBlank String holderName) { /* validation only */ }
record AccountResponse(String id, BigDecimal balance) { /* transport only */ }
What about lazy-loading bugs from exposing entities?
When the controller returns an entity and the JSON serializer runs after the transaction has closed, touching a lazy relationship like account.getTransactions() throws LazyInitializationException. Mapping to a DTO inside the transaction, copying only the fields you need, avoids this because the serializer never touches the entity.
// This crashes outside a transaction:
@GetMapping("/accounts/{id}")
public Account get(@PathVariable String id) {
return repo.findById(id).orElseThrow();
// Jackson touches account.transactions → LazyInitializationException
}
// This works – DTO created inside the service transaction:
@GetMapping("/accounts/{id}")
public AccountResponse get(@PathVariable String id) {
return accountService.getAccount(id); // maps to DTO inside transaction
}
What should a senior engineer know?
Keeping the three roles separate is a hallmark of a well-layered banking application. Interviewers look for whether you understand why the separation exists, not just that it does. The entity stays below the service boundary. The DTO crosses the API boundary with validation. The POJO holds domain logic that is pure, testable, and reusable. Tools like MapStruct generate the entity-to-DTO mapping at compile time, so the common objection of too much boilerplate does not hold.
A senior also knows that in a hexagonal or ports-and-adapters architecture, the POJO domain model sits at the center, free of all framework dependencies. The entity is an adapter for persistence, and the DTO is an adapter for the API. This separation lets you swap the persistence technology or the API format without touching the domain logic.
Visual Overview
Request Flow Through Layers:
Client
│
│ TransferRequest (DTO)
▼
┌──────────────┐
│ Controller │ receives DTO, validates
└──────┬───────┘
│
▼
┌──────────────┐
│ Service │ maps DTO → Entity, uses POJO for logic
│ │ ┌──────────┐ ┌──────────┐
│ │ │ Money │ │ Account │
│ │ │ (POJO) │ │ (Entity) │
│ │ └──────────┘ └──────────┘
└──────┬───────┘
│
│ Entity saved via Repository
▼
┌──────────────┐
│ Database │
└──────────────┘
Response: Entity → mapped to TransferResponse (DTO) → Client
Key Takeaway
An Entity, a DTO, and a POJO are three different kinds of object with three different responsibilities. The Entity is a JPA-managed class that maps to a database table and is tracked by the persistence context, so it belongs to the persistence layer and should never be exposed directly. The DTO is a plain class or record that carries data across layers or the API boundary, with validation annotations but no business logic. The POJO is any framework-free object that may hold domain logic, like a Money value object.
The distinction matters because each has risks if misused. At National Bank of India, early code used a single Account class with JPA annotations, validation annotations, and returned it directly to clients. That caused a LazyInitializationException when the serializer touched an unloaded transactions relationship, leaked the password hash and internal risk score, and coupled the API to the schema. Separating an internal entity from a response DTO fixed all three at once.
The POJO earns its place in the domain logic. The team had interest calculation mixed into the entity, which made it hard to test because it dragged in persistence concerns. Moving that into a framework-free Money value object with an applyInterest method using BigDecimal let the team unit test the logic with no database at all, and reuse it anywhere.
When deciding which to use, follow the layer. In the repository and persistence layer, use entities because that is what JPA manages. Across the API boundary and between layers, use DTOs because they are a stable, validated, decoupled contract. For domain rules and value objects, use POJOs so the logic stays pure and testable.
Remember one sentence: entities persist, DTOs transport, POJOs think, and keeping the three separate is what makes a banking application well layered, secure, and easy to evolve.
---