Skip to content

9.4. Service Implementation Pattern

9.4.1. StorageService Interface

java
public interface StorageService {

  FileResponse upload(MultipartFile file, Long uploadedBy);

  FileDownload download(Long fileId);

  void delete(Long fileId);
}

9.4.2. Implementation Structure

java
@Service
@RequiredArgsConstructor
public class LocalStorageService implements StorageService {

  private final StorageProperties properties;
  private final FileMetadataRepository fileMetadataRepository;
  private final FileValidator fileValidator;

  @Override
  @Transactional
  public FileResponse upload(MultipartFile file, Long uploadedBy) {
    fileValidator.validate(file);

    String storedFilename = FileNameGenerator.generate(file.getOriginalFilename());
    Path targetPath = StoragePath.resolve(properties.uploadPath(), storedFilename);

    try {
      Files.createDirectories(targetPath.getParent());
      Files.copy(file.getInputStream(), targetPath);
    } catch (IOException e) {
      throw new StorageException("Failed to store the file.", e);
    }

    FileMetadataRecord record = fileMetadataRepository.insert(
        file.getOriginalFilename(),
        storedFilename,
        targetPath.toString(),
        file.getContentType(),
        file.getSize(),
        uploadedBy
    );

    return FileResponse.from(record);
  }

  @Override
  @Transactional(readOnly = true)
  public FileDownload download(Long fileId) {
    FileMetadataRecord metadata = fileMetadataRepository.findById(fileId)
        .orElseThrow(() -> new FileNotFoundException(fileId));

    Path filePath = Path.of(metadata.getFilePath());
    Resource resource = new FileSystemResource(filePath);

    if (!resource.exists()) {
      throw new FileNotFoundException(fileId);
    }

    return new FileDownload(
        resource,
        metadata.getOriginalFilename(),
        metadata.getContentType()
    );
  }

  @Override
  @Transactional
  public void delete(Long fileId) {
    FileMetadataRecord metadata = fileMetadataRepository.findById(fileId)
        .orElseThrow(() -> new FileNotFoundException(fileId));

    try {
      Files.deleteIfExists(Path.of(metadata.getFilePath()));
    } catch (IOException e) {
      log.error("Failed to delete file: fileId={}", fileId, e);
    }

    fileMetadataRepository.deleteById(fileId);
  }
}

Extension

The current implementation uses the local file system, but when transitioning to cloud storage (e.g., S3) in the future, only a new implementation of the StorageService interface needs to be added. The interface-based design allows the storage strategy to be changed flexibly.

TIENIPIA QUALIFIED STANDARD