Mastering PO, VO, BO, DTO, DAO: When to Use Each Java Object

This article explains the definitions, responsibilities, and differences of six common Java objects—PO, VO, BO, DTO, DAO, and POJO—illustrates their typical usage in layered architectures, presents conversion models and tools like MapStruct and Dozer, and offers practical guidelines to avoid common pitfalls.

Architect
Architect
Architect
Mastering PO, VO, BO, DTO, DAO: When to Use Each Java Object

Preface

Recently a friend asked me: PO, VO, BO, DTO, DAO, POJO – what are the differences? At first glance they can be confusing, and their concepts are easy to mix up. This article discusses the meaning, responsibilities, distinctions, and common pitfalls of these six objects to help you understand them better.

1. Responsibilities of the 6 Objects

Object design’s essence is separation of concerns—each object does one thing and does it well!

1.1 PO

PO stands for Persistent Object.

Responsibility: Strict 1:1 mapping with a database table, only carries data storage structure.

Feature:

Attributes correspond exactly to table fields.

No business logic methods (only getters/setters).

Code Example:

public class UserPO {
    private Long id; // corresponds to primary key
    private String name; // corresponds to name column
}

1.2 DAO

DAO stands for Data Access Object.

Responsibility: Encapsulate all database operations (CRUD), isolate business logic from storage details.

Feature:

Interface methods correspond to SQL operations.

Return PO or collections of PO.

Code Example:

public interface UserDao {
    // Query PO by ID
    UserPO findById(Long id);
    // Pagination query
    List<UserPO> findPage(@Param("offset") int offset, @Param("limit") int limit);
}

Underlying principle: DAO pattern = interface + implementation + PO.

1.3 BO

BO stands for Business Object.

Responsibility: Encapsulate core business logic, aggregate multiple PO to complete complex operations.

Feature:

Contains business state machine, validation rules.

Can hold references to multiple PO.

Code Example: Order refund BO

public class OrderBO {
    // Main order data
    private OrderPO orderPO;
    // Sub order items
    private List<OrderItemPO> items;
    // Business method: execute refund
    public RefundResult refund(String reason) {
        if (!"PAID".equals(orderPO.getStatus())) {
            throw new IllegalStateException("Unpaid order cannot be refunded");
        }
        // calculate refund amount, call payment gateway, etc.
    }
}

1.4 DTO

DTO stands for Data Transfer Object.

Responsibility: Transfer data across layers/services, hide sensitive fields.

Feature:

Attribute set is a subset of PO (e.g., excludes password).

Supports serialization (implements Serializable).

Code Example: User information DTO

public class UserDTO implements Serializable {
    private Long id;
    private String name;
}

1.5 VO

VO stands for View Object.

Responsibility: Adapt data for front‑end display, may contain rendering logic.

Feature:

Attributes can include formatted data (e.g., yyyy‑MM‑dd).

Aggregates data from multiple tables (e.g., order VO contains user name).

Code Example:

public class OrderVO {
    private String orderNo;
    private String createTime; // formatted date
    private String userName; // from user table
    public String getStatusText() {
        return OrderStatus.of(this.status).getDesc();
    }
}

1.6 POJO

POJO stands for Plain Old Java Object.

Responsibility: Basic data container, can play the role of PO/DTO/VO.

Feature:

Only fields + getters/setters.

No framework dependencies (does not extend Spring classes).

Typical implementation: Lombok simplifies code.

@Data
public class UserPOJO {
    private Long id;
    private String name;
}

2. Main Object Flow Models

Scenario 1

Traditional three‑layer architecture (DAO → DTO → VO). Suitable for back‑office systems and utility applications.

Code Example: User query service

// Service layer
public UserDTO getUserById(Long id) {
    UserPO userPO = userDao.findById(id); // fetch PO
    UserDTO dto = new UserDTO();
    dto.setId(userPO.getId());
    dto.setName(userPO.getName()); // filter sensitive fields
    return dto;
}
// Controller layer
public UserVO getUser(Long id) {
    UserDTO dto = userService.getUserById(id);
    UserVO vo = new UserVO();
    vo.setUserId(dto.getId());
    vo.setUserName(dto.getName());
    vo.setRegisterTime(formatDate(dto.getCreateTime())); // format date
    return vo;
}

Pros: Simple and direct, suitable for CRUD scenarios.

Cons: Business logic can leak into the Service layer.

Scenario 2

DDD architecture (PO → DO → DTO → VO). Suitable for complex domains such as e‑commerce and finance.

Key role: DO (Domain Object) replaces BO.

Code Example: Order payment domain

// Domain layer: order domain object
public class OrderDO {
    private OrderPO orderPO;
    private PaymentPO paymentPO;
    // Business method: payment validation
    public void validatePayment() {
        if (paymentPO.getAmount() < orderPO.getTotalAmount()) {
            throw new PaymentException("Insufficient payment amount");
        }
    }
}
// Application layer: coordinate domain objects
public OrderPaymentDTO pay(OrderPayCmd cmd) {
    OrderDO order = orderRepo.findById(cmd.getOrderId());
    order.validatePayment(); // invoke domain method
    return OrderConverter.toDTO(order); // convert to DTO
}

Pros: High cohesion of business logic, suitable for complex rule systems.

Cons: More conversion layers increase development cost.

3. Efficient Conversion Tools

3.1 MapStruct – compile‑time code generation

Principle: Annotation processor generates mapping code.

Example: PO to DTO conversion

@Mapper
public interface UserConverter {
    UserConverter INSTANCE = Mappers.getMapper(UserConverter.class);
    @Mapping(source = "createTime", target = "registerDate")
    UserDTO poToDto(UserPO po);
}
// Generated implementation
public class UserConverterImpl implements UserConverter {
    public UserDTO poToDto(UserPO po) {
        UserDTO dto = new UserDTO();
        dto.setRegisterDate(po.getCreateTime()); // automatic assignment
        return dto;
    }
}

Pros: No reflection overhead, performance close to hand‑written code.

Open‑source: https://github.com/mapstruct/mapstruct

3.2 Dozer + Lombok – annotation‑driven conversion

Combination:

Lombok – auto‑generate getters/setters.

Dozer – XML/annotation based field mapping.

// Lombok annotation
@Data
public class UserVO {
    private String userId;
    private String userName;
}
// Mapping configuration
<field>
    <a>userId</a>
    <b>id</b>
</field>

Applicable scenario: Complex conversions where field names differ.

3.3 Manual Builder Pattern – fine‑grained control

Applicable scenario: Dynamically constructed VO.

public class OrderVOBuilder {
    public OrderVO build(OrderDTO dto) {
        return OrderVO.builder()
            .orderNo(dto.getOrderNo())
            .amount(dto.getAmount() + "元") // dynamic concatenation
            .statusText(convertStatus(dto.getStatus()))
            .build();
    }
}

4. Pitfall Guide

Pitfall 1: Returning PO directly to the front‑end

// Fatal error: exposing DB sensitive fields!
public UserPO getUser(Long id) {
    // PO contains password
    return userDao.findById(id);
}

Solution:

Use DTO to filter fields.

Apply @JsonIgnore on sensitive properties.

Pitfall 2: Embedding business logic inside DTO

public class OrderDTO {
    // Wrong! DTO should not contain business methods
    public void validate() {
        if (amount < 0) {
            throw new Exception();
        }
    }
}

Root cause: Confusing responsibilities of DTO and BO.

Pitfall 3: Circular nested conversion causing N+1 queries

public class OrderVO {
    private List<ProductVO> products; // nested objects
}
orderVO.setProducts(order.getProducts()
    .stream()
    .map(p -> convertToVO(p)) // triggers N+1 DB queries
    .collect(toList()));

Optimization: Batch queries + parallel conversion.

5. How to Choose an Object Model?

Summary

Single Responsibility: PO stores data, BO handles business, VO handles presentation – never cross boundaries.

Safety Isolation:

PO never leaves DAO layer (prevent DB leakage).

VO never leaves Controller (prevent front‑end logic pollution).

Performance First:

Use MapStruct for large‑scale object conversion (compile‑time generated code).

Use batch queries for nested collections to avoid N+1.

Moderate Design:

Systems with ≤10 tables can use POJO throughout.

Core systems with >100 tables must enforce strict layering.

There is no silver bullet for object design; understanding the business is more important than blindly applying patterns.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaBackend DevelopmentdtodaoPOvo
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.