What Is a DTO? (And Why You Shouldn’t Return Your Entities in Spring Boot)

When building REST APIs with Spring Boot, it’s common to see beginners return entities directly from their controllers. At first glance, this seems fine—the data flows, the response looks right, and everything “just works.”

But what happens when your entity evolves? When sensitive fields accidentally get exposed? Or when lazy-loaded relationships break your JSON?

This is where DTOs (Data Transfer Objects) become essential. In this post, you’ll learn what DTOs are, why returning entities is a bad practice, and how to design a clean service layer that uses DTOs as the only communication contract between your controller and your core domain.

No frameworks. No magic. Just clean code, solid design, and a scalable approach.

What Is a DTO?

A DTO (Data Transfer Object) is a simple data structure whose sole responsibility is to carry data across layers of your application.

In Spring Boot applications, DTOs are commonly used to:

  • Receive input from the client (POST/PUT requests)
  • Return structured output to the client (GET/DELETE responses)

Unlike JPA entities, DTOs are not tied to the database. They represent the shape of the data you want to expose, and that shape is entirely up to you.

With Java 14+, DTOs can be defined concisely using records:

public record UserDTO(String name, String email) {}

Why You Shouldn’t Return Entities from Controllers

Returning JPA entities directly from your controllers creates a fragile and unsafe architecture. Here’s why:

1. Sensitive Data Exposure

Entities often contain fields that should never reach the client. Examples:

  • Password hashes
  • Internal flags
  • Audit timestamps
  • Foreign key relationships

If you return an entity, you risk leaking private or irrelevant data.

2. Tight Coupling to the Database Schema

Entities reflect how your data is stored—not how it should be presented. If your frontend depends on your entity structure, any change in the database can break your client contracts.

By returning your entities directly, your DB model becomes your API contract.

3. Serialization and Lazy Loading Issues

JPA entities with @OneToMany or @ManyToOne relationships are often lazily loaded. If Jackson tries to serialize a proxy object, you’ll hit exceptions like:

com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection

Or worse: infinite recursion due to circular references.

4. Performance Overhead

Entities tend to carry too much data. DTOs allow you to shape and minimize your payload for exactly what the client needs—nothing more.

The Clean Approach: DTO In, DTO Out (Controller <-> Service)

Let’s walk through a real-world example where:

  • Controllers deal only with DTOs
  • Services handle all the mapping
  • Entities stay hidden from external layers

Example: Clean Architecture with DTOs

Step 1: Define the JPA Entity

@Entity
public class User {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private String email;
    private String password;
    private LocalDateTime createdAt;

    // Getters and setters
}

Note: This class includes fields (id, password, createdAt) that we don’t want to expose via the API.

Step 2: Define DTOs as Records

public record CreateUserRequest(String name, String email, String password) {}

public record UserResponse(String name, String email) {}
  • CreateUserRequest will be used in the POST /users endpoint.
  • UserResponse will be returned from GET and POST operations.

Step 3: Implement the Repository

public interface UserRepository extends JpaRepository<User, Long> {}

Spring will generate the implementation automatically.

Step 4: Implement the Service

@Service
public class UserService {

    private final UserRepository repository;

    public UserService(UserRepository repository) {
        this.repository = repository;
    }

    public UserResponse createUser(CreateUserRequest request) {
        User user = new User();
        user.setName(request.name());
        user.setEmail(request.email());
        user.setPassword(request.password()); // Assume hashed later
        user.setCreatedAt(LocalDateTime.now());

        User saved = repository.save(user);
        return toResponse(saved);
    }

    public UserResponse getUserById(Long id) {
        User user = repository.findById(id)
            .orElseThrow(() -> new RuntimeException("User not found"));
        return toResponse(user);
    }

    private UserResponse toResponse(User user) {
        return new UserResponse(user.getName(), user.getEmail());
    }
}

✅ Mapping is explicit
✅ Entity is isolated
✅ DTOs are the only exposed format

Step 5: Implement the Controller

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService service;

    public UserController(UserService service) {
        this.service = service;
    }

    @PostMapping
    public ResponseEntity<UserResponse> createUser(@RequestBody CreateUserRequest request) {
        UserResponse response = service.createUser(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }

    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
        return ResponseEntity.ok(service.getUserById(id));
    }
}

The controller doesn’t touch the entity. It only:

  • Accepts DTOs
  • Returns DTOs
  • Delegates all business logic to the service

Advantages of This Pattern

Encapsulation
The controller and the client don’t know or care how data is stored.

Security
Sensitive fields are never exposed, even accidentally.

Maintainability
You can evolve your entities or DTOs independently.

Testability
Service methods are easily testable by verifying DTO inputs and outputs.


Final Thoughts

Returning entities from your Spring Boot controllers might seem like a shortcut—but it’s one that comes with long-term cost.

Using DTOs:

  • Gives you full control over your API contract
  • Keeps your domain model protected
  • Helps you avoid common pitfalls like serialization errors and tight coupling

You don’t need external libraries or mapping frameworks to get this right. With a bit of manual work and clear boundaries between layers, your Spring Boot apps will be cleaner, safer, and easier to evolve.

Remember: Controllers speak in DTOs. Services do the translation. Entities stay behind the curtain.

Leave a Reply

Your email address will not be published. Required fields are marked *