Search code examples
javaspringspring-boot

Best practice for fetching common metadata for entities in Spring Boot


I have a BaseEntity class in my Spring Boot application that includes common fields for all entities:

@Data
@MappedSuperclass
public class BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String creatorId;
    private ZonedDateTime createTime;
    private String updaterId;
    private ZonedDateTime updateTime;
    private boolean deleted;
}

Each entity in the application inherits from BaseEntity. When displaying the create-update information for any entity, I need to map the creatorId and updaterId to full names from the User table.

To achieve this, I created an API for each entity that retrieves the create-update info using a repository query specific to that entity. However, this leads to duplicated logic across multiple APIs and service methods.

To avoid duplication, I tried to create a single API, getCreateUpdateInfo(), that accepts an entity ID and a type (to identify the entity). In a shared service, I used a switch statement to call the appropriate repository query based on the type.

Example of the shared service logic:

public CreateUpdateInfoDto getCreateUpdateInfo(IdTypeEnumDto dto) {
    return switch (dto.getTypeEnum()) {
        case USER -> userRepository.getCreateUpdateInfo(dto.getId());
        case ORDER -> orderRepository.getCreateUpdateInfo(dto.getId());
        case PROJECT -> projectRepository.getCreateUpdateInfo(dto.getId());
        default -> throw new IllegalArgumentException("Unhandled entity type: " + dto.getTypeEnum());
    };
}

While this reduces redundancy, I am not entirely satisfied with the switch-based design... Is it better to keep separate endpoints for retrieving create-update metadata for each entity, or is there a more efficient way to implement a shared logic without duplication?


Solution

  • To implement a strategy pattern where each entity type has its own handler for retrieving create-update metadata, strategy pattern can be considered. It allows you to delegate the responsibility of fetching the data to specific strategy implementations.

    Here is the solution way shown below

    1 ) Define an Interface for Handlers

    public interface CreateUpdateInfoHandler {
        boolean supports(IdTypeEnum type);
        CreateUpdateInfoDto getCreateUpdateInfo(Long id);
    }
    

    2 ) Implement Handlers for Each Entity

    @Service
    public class UserCreateUpdateInfoHandler implements CreateUpdateInfoHandler {
        private final UserRepository userRepository;
    
        public UserCreateUpdateInfoHandler(UserRepository userRepository) {
            this.userRepository = userRepository;
        }
    
        @Override
        public boolean supports(IdTypeEnum type) {
            return type == IdTypeEnum.USER;
        }
    
        @Override
        public CreateUpdateInfoDto getCreateUpdateInfo(Long id) {
            return userRepository.getCreateUpdateInfo(id);
        }
    }
    
    @Service
    public class OrderCreateUpdateInfoHandler implements CreateUpdateInfoHandler {
        private final OrderRepository orderRepository;
    
        public OrderCreateUpdateInfoHandler(OrderRepository orderRepository) {
            this.orderRepository = orderRepository;
        }
    
        @Override
        public boolean supports(IdTypeEnum type) {
            return type == IdTypeEnum.ORDER;
        }
    
        @Override
        public CreateUpdateInfoDto getCreateUpdateInfo(Long id) {
            return orderRepository.getCreateUpdateInfo(id);
        }
    }
    

    3 ) Register and Use Handlers in the Shared Service

    @Service
    public class CreateUpdateInfoService {
        private final List<CreateUpdateInfoHandler> handlers;
    
        public CreateUpdateInfoService(List<CreateUpdateInfoHandler> handlers) {
            this.handlers = handlers;
        }
    
        public CreateUpdateInfoDto getCreateUpdateInfo(IdTypeEnumDto dto) {
            return handlers.stream()
                    .filter(handler -> handler.supports(dto.getTypeEnum()))
                    .findFirst()
                    .orElseThrow(() -> new IllegalArgumentException("Unhandled entity type: " + dto.getTypeEnum()))
                    .getCreateUpdateInfo(dto.getId());
        }
    }
    

    4 ) Endpoints in a controller to fetch create-update info for any entity

    @RestController
    @RequestMapping("/api/v1")
    public class MetadataController {
        private final CreateUpdateInfoService createUpdateInfoService;
    
        public MetadataController(CreateUpdateInfoService createUpdateInfoService) {
            this.createUpdateInfoService = createUpdateInfoService;
        }
    
        @GetMapping("/create-update-info")
        public ResponseEntity<CreateUpdateInfoDto> getCreateUpdateInfo(@RequestParam Long id, @RequestParam IdTypeEnum type) {
            IdTypeEnumDto dto = new IdTypeEnumDto(id, type);
            return ResponseEntity.ok(createUpdateInfoService.getCreateUpdateInfo(dto));
        }
    }
    

    Here is the sample request shown below

    /api/v1/create-update-info?id=123&type=USER