Why interviewers ask this: It quickly shows whether you understand the boundary between your REST API and your database model.
Question: DTO vs Entity.
Answer: An Entity is the object JPA or Hibernate uses to represent data in the database. A DTO (Data Transfer Object) is a simpler object used to send or receive data at the API boundary. In Spring Boot, I usually keep entities inside the persistence layer and convert them to DTOs for controllers, so I do not leak fields like passwords, audit data, or lazy-loaded relationships.
Interview-Ready Answer: I use an entity to model how data is stored and managed by JPA, and I use a DTO to model what my API accepts or returns. In Spring Boot, I keep entities behind the service layer and map them to request and response DTOs at the controller boundary. That gives me a safer API, avoids leaking internal fields, and makes it easier to change the database without breaking clients.
Detailed Explanation:
An Entity is the class JPA or Hibernate manages as a database record. It usually has @Entity, an @Id, and fields that match stored columns or relationships. A DTO is a plain object used to move data across a boundary, like a REST request or response; it should contain only the data that the client actually needs.
| Aspect | Entity | DTO |
|---|---|---|
| Job | Persist data | Transfer data |
| JPA aware | Yes | No |
| Safe to expose | Often no | Usually yes |
| Shape | Database-driven | API-driven |
| Lifecycle | Managed by JPA | Simple object |
Entity for saving, loading, relationships, and database rules.DTO for API inputs, API outputs, versioning, and hiding internal fields.n objects, the cost is O(n). For one object, the cost is effectively O(1) per field copy.LazyInitializationException if the session is already closed.jakarta.* packages; Spring Boot 2 uses javax.*. The idea stays the same, but imports change.Rule of thumb: entities are for the backend’s internal truth; DTOs are for the outside world. If the object crosses a network boundary, make it a DTO.
Real-World Example: Imagine an e-commerce checkout service. The OrderEntity contains internal fields like costPrice, fraudScore, warehouse notes, and a lazy-loaded list of line items. The API should return only a clean OrderResponse with order id, status, items, and total amount. One team once returned the entity directly to save time; in production, that leaked internal margin data to clients and occasionally crashed with LazyInitializationException when the session was already closed. The symptom was ugly: random 500 errors, huge JSON payloads, and support tickets from customers who could not open order details. The fix was simple but important: keep the entity in the service layer, map to a DTO, and make the controller speak only DTOs.
package com.example.dtoentity;\n\nimport java.nio.charset.StandardCharsets;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.time.Instant;\nimport java.util.Map;\nimport java.util.NoSuchElementException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicLong;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.stereotype.Service;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@SpringBootApplication\npublic class DtoVsEntityApplication {\n public static void main(String[] args) {\n SpringApplication.run(DtoVsEntityApplication.class, args);\n }\n}\n\n@RestController\n@RequestMapping("/api/users")\nclass UserController {\n private final UserService userService;\n\n UserController(UserService userService) {\n this.userService = userService;\n }\n\n @PostMapping\n public ResponseEntity<UserResponse> create(@RequestBody CreateUserRequest request) {\n // Request DTO = the public API shape. We validate it before touching persistence.\n UserResponse created = userService.create(request);\n return ResponseEntity.status(HttpStatus.CREATED).body(created);\n }\n\n @GetMapping("/{id}")\n public ResponseEntity<UserResponse> get(@PathVariable long id) {\n // Response DTO = what we intentionally expose to the client.\n return ResponseEntity.ok(userService.findResponseById(id));\n }\n\n @GetMapping("/raw/{id}")\n public ResponseEntity<UserEntity> raw(@PathVariable long id) {\n // This endpoint is intentionally wrong for real systems.\n // It shows why returning entities can leak internal fields like passwordHash.\n return ResponseEntity.ok(userService.findEntityById(id));\n }\n\n @ExceptionHandler(IllegalArgumentException.class)\n public ResponseEntity<Map<String, String>> badRequest(IllegalArgumentException ex) {\n return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));\n }\n\n @ExceptionHandler(NoSuchElementException.class)\n public ResponseEntity<Map<String, String>> notFound(NoSuchElementException ex) {\n return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", ex.getMessage()));\n }\n}\n\n@Service\nclass UserService {\n private final AtomicLong sequence = new AtomicLong(1000);\n private final Map<Long, UserEntity> store = new ConcurrentHashMap<>();\n\n public UserResponse create(CreateUserRequest request) {\n validate(request);\n\n long id = sequence.incrementAndGet();\n UserEntity entity = new UserEntity();\n entity.setId(id);\n entity.setFullName(request.fullName().trim());\n entity.setEmail(request.email().trim().toLowerCase());\n entity.setPasswordHash(hashPassword(request.password())); // store a hash, never the raw password\n entity.setCreatedAt(Instant.now());\n\n store.put(id, entity);\n return toResponse(entity);\n }\n\n public UserResponse findResponseById(long id) {\n return toResponse(findEntityById(id));\n }\n\n public UserEntity findEntityById(long id) {\n UserEntity entity = store.get(id);\n if (entity == null) {\n throw new NoSuchElementException("User " + id + " not found");\n }\n return entity;\n }\n\n private static void validate(CreateUserRequest request) {\n if (request == null) {\n throw new IllegalArgumentException("Request body is required");\n }\n if (isBlank(request.fullName())) {\n throw new IllegalArgumentException("fullName is required");\n }\n if (isBlank(request.email()) || !request.email().contains("@")) {\n throw new IllegalArgumentException("email must contain @");\n }\n if (request.password() == null || request.password().length() < 8) {\n throw new IllegalArgumentException("password must be at least 8 characters");\n }\n }\n\n private static boolean isBlank(String value) {\n return value == null || value.trim().isEmpty();\n }\n\n private static UserResponse toResponse(UserEntity entity) {\n // Mapping keeps the API stable even if the entity grows new columns later.\n return new UserResponse(entity.getId(), entity.getFullName(), entity.getEmail(), entity.getCreatedAt());\n }\n\n private static String hashPassword(String rawPassword) {\n // Demo only: use BCrypt or Argon2 in production.\n try {\n MessageDigest md = MessageDigest.getInstance("SHA-256");\n byte[] digest = md.digest(rawPassword.getBytes(StandardCharsets.UTF_8));\n StringBuilder hex = new StringBuilder();\n for (byte b : digest) {\n hex.append(String.format("%02x", b));\n }\n return "sha256:" + hex;\n } catch (NoSuchAlgorithmException ex) {\n throw new IllegalStateException("SHA-256 not available", ex);\n }\n }\n}\n\nclass UserEntity {\n private Long id;\n private String fullName;\n private String email;\n private String passwordHash;\n private Instant createdAt;\n\n public Long getId() { return id; }\n public void setId(Long id) { this.id = id; }\n public String getFullName() { return fullName; }\n public void setFullName(String fullName) { this.fullName = fullName; }\n public String getEmail() { return email; }\n public void setEmail(String email) { this.email = email; }\n public String getPasswordHash() { return passwordHash; }\n public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }\n public Instant getCreatedAt() { return createdAt; }\n public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }\n}\n\nrecord CreateUserRequest(String fullName, String email, String password) {}\n\nrecord UserResponse(Long id, String fullName, String email, Instant createdAt) {}Follow-up & Tricky Questions:
@JsonIgnore enough instead of DTOs? No. It only hides one field from JSON. It does not fix tight coupling, lazy-loading problems, or awkward API shapes.Common Mistakes:
Memory Hook: Entity is the warehouse pallet; DTO is the shipping box. Keep the pallet in the warehouse, and ship only the box to the customer.
Cheat Sheet:
jakarta.*; Boot 2 uses javax.*.Practice Tasks:
UpdateUserRequest DTO.