Backend Development 20 min read

Refactoring Cloud Disk Upload Logic with Domain-Driven Design: Models, Factories, and Repositories

The article examines the difficulties of applying Domain-Driven Design to a cloud‑disk file‑upload scenario, demonstrates how to simplify the code by extracting business models, introducing factories and repositories, and explains how these patterns clarify the separation between business and technical concerns.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Refactoring Cloud Disk Upload Logic with Domain-Driven Design: Models, Factories, and Repositories
❝ The principles of Domain‑Driven Design are abstract and hard to grasp, with many obscure concepts; during my study I encountered considerable resistance, so in this article I set aside all the terminology to share my personal learning experience of DDD. ❞

Model is an abstraction of business

Below is a realistic business scenario simplified for better understanding:

A user uploads a local file to a personal cloud disk; the source data resides on the user's device, and we need to logically store the file in the user's personal cloud disk.

public void buildSendFileRelation(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
        // Parameter validation
        checkSceneParam(sceneDTO);
        checkNodeParams(nodeDTO);

        // Pre‑processing parameters
        sceneDTO = convertAndSetParamsBeforeUpLoad(sceneDTO, nodeDTO);

        // Query DB to confirm the personal cloud node (technical language)
        Long fromAliId = Long.valueOf(sceneDTO.getFrom());
        Long toAliId = Long.valueOf(sceneDTO.getTo());
        NodeDTO privateRootDir = filePrivateChatDirComponent.getAndCreatePrivateRootDir(fromAliId, toAliId, true);

        // Fill parameters based on personal cloud info
        convertAndSetParamsBeforeUpload(nodeDTO, privateRootDir, fromAliId);

        // File name encoding (technical language)
        if (chatScene != ChatScenceTypeEnum.PERSON_CLOUD.getValue() && RegexpUtil.isContainIllegalChar(file.getNodeName())) {
            file.setNodeName(UrlCodingUtil.encode(file.getNodeName()));
        }

        // Check OSS source data (technical language)
        checkOss(chatScene, nodeDTO)

        // Prepare parameters based on OSS data
        prepareNodeBeforeInsert(chatScene, nodeDTO);

        // File safety review (business language)
        fileSafeManager.submitSafeScan(nodeDTO);

        // Insert file data into DB (technical? business? language)
        cloudNodeRepository.insertFile(nodeDTO);
 }

Such code is common in our applications; in reality the logic is scattered across many services and managers, and when we gather the fragmented logic together we obtain the example above.

Analyzing the example reveals that the processing follows a procedural approach, which leads to two obvious problems:

Redundant data preparation and filling (personal cloud info is fetched once, then file source data is fetched again).

Technical and business operations are tangled together throughout the chain, causing repeated context switches.

These issues make it difficult to understand the code’s processing flow.

When we analyse the business scenario, the overall workflow is actually very simple:

User reports file information.

File information is stored in the personal cloud disk.

File undergoes safety review.

Only two main objects and one service are involved: the File object, the PersonalCloud object, and the SafetyReview service. Re‑arranging the code according to this logic yields:

public void buildSendFileRelation(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
        // Create file and personal cloud objects
        File file = createFile(nodeDTO, sceneDTO, params);
        Clouddisk clouddisk = createClouddisk(nodeDTO, sceneDTO, params);

        // Safety service scans file
        safeService.asynScan(file);

        // Save file to personal cloud
        clouddisk.save(file);
 }

File createFile(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
    checkOss(chatScene, nodeDTO);
    file = prepareNodeBeforeInsert(chatScene, nodeDTO);
    return file;
}

Clouddisk createClouddisk(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
    // Query DB to confirm personal cloud node
    Long fromAliId = Long.valueOf(sceneDTO.getFrom());
    Long toAliId = Long.valueOf(sceneDTO.getTo());
    NodeDTO privateRootDir = filePrivateChatDirComponent.getAndCreatePrivateRootDir(fromAliId, toAliId, true);

    // Fill parameters based on personal cloud info
    return convertAndSetParamsBeforeUpload(nodeDTO, privateRootDir, fromAliId);
}

Compared with the original, the core processing chain is clearer.

"An abstract model replaces technical language with business language, reducing code confusion; in DDD the abstract model corresponds to Entities, Value Objects, or Services in OOP."

Introducing Factory Classes and Repositories

Even after clarifying the core chain, object creation may still carry its own business or technical rules, which are not fully decoupled from the main flow in the previous example.

We can use factory classes to separate business from data acquisition:

public void buildSendFileRelation(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
     // Create file and personal cloud objects
     File file = FileFactory.createFile(nodeDTO, sceneDTO, params);
     Clouddisk clouddisk = ClouddiskFactory.createClouddisk(nodeDTO, sceneDTO, params);

     // Safety service scans file
     safeService.asynScan(file);

     // Save file to personal cloud
     clouddisk.save(file);
 }
Class FileFactory {
   public File createFile(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
    // Query OSS data source
    checkOss(chatScene, nodeDTO);
    // Data filling
    file = prepareNodeBeforeInsert(chatScene, nodeDTO);
    return file;
   }
}

Class ClouddiskFactory {
  public Clouddisk createClouddisk(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
    // Query DB to confirm personal cloud node
    Long fromAliId = Long.valueOf(sceneDTO.getFrom());
    Long toAliId = Long.valueOf(sceneDTO.getTo());
    NodeDTO privateRootDir = filePrivateChatDirComponent.getAndCreatePrivateRootDir(fromAliId, toAliId, true);

    // Fill parameters based on personal cloud info
    return convertAndSetParamsBeforeUpload(nodeDTO, privateRootDir, fromAliId);
  }
}

The factories now handle material acquisition (data queries) and assembly (object instantiation). Adding repositories to encapsulate data queries further isolates the core logic:

Class FileFactory {
   public File createFile(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
    // Query OSS data source
    ossObj = ossRepository.getOssObject(nodeDTO, privateRootDir, fromAliId);
    // Data filling
    return convertAndSetParamsBeforeUpload(nodeDTO, ossObj);
   }
}

Class ClouddiskFactory {
  public Clouddisk createClouddisk(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
    // Query DB to confirm personal cloud node
    cloudNode = cloudNodeRepository.getCloudNode(nodeDTO, privateRootDir, fromAliId);
    // Fill parameters based on personal cloud info
    return convertAndSetParamsBeforeUpload(nodeDTO, cloudNode);
  }
}

Interfect OssRepository {
    public Object getOssObject(NodeDTO nodeDTO, SceneDTO sceneDTO, String params);
}

Interfect CloudNodeRepository {
    public Object getCloudNode(NodeDTO nodeDTO, SceneDTO sceneDTO, String params);
}

"Factories and repositories exist to create business instances while shielding the main flow from extra creation rules."

Dependency Relationships Between Layers

Business instances, factories, and repositories together form the most basic domain structure (i.e., the business layer). In our cloud‑disk example, the current code still lets DTO objects (NodeDTO, SceneDTO) leak into the domain layer.

Our ultimate goal is that within a business domain no definitions or semantics unrelated to that domain appear. Therefore, we must hide unrelated objects at the application layer:

public void buildSendFileRelation(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
     // Create file and personal cloud objects
     File file = FileFactory.createFile(nodeDTO.getId(), nodeDTO.getParentId());
     Clouddisk clouddisk = ClouddiskFactory.createClouddisk(sceneDTO.getALiId);

     // Safety service scans file
     safeService.scan(file);

     // Save file to personal cloud
     clouddisk.save(file);
 }
// Entity VO Service
Class File {
    // ...
}

Class Clouddisk {
    // ...
}

Class SafeService {
   public void scan(File file) {
       // ...
   }
}

// Factory
Class FileFactory {
   public File createFile(Long id, Long parentId, String ossKey) {
    Object ossObj =  ossRepository.getOssObject(ossKey);
    file = new File(id, parentId, ossObj);
    return file;
   }
}

Class ClouddiskFactory {
  public Clouddisk createClouddisk(Long fromAliId) {
    Object cloudNode = cloudNodeRepository.getCloudNode(fromAliId);
    Clouddisk clouddisk = new Clouddisk(fromAliId, cloudNode);
    return clouddisk;
  }
}

// Repository
Interfect OssRepository {
    public Object getOssObject(Long id, Long parentId, String ossKey);
}

Interfect CloudNodeRepository {
    public Object getCloudNode(Long fromAliId);
}

"The business model (domain layer) should not depend on objects or capabilities defined in the application layer; it must be a closed, self‑sufficient loop."

Nevertheless, business instances need external services or data sources. The repository pattern solves this by exposing only interfaces inside the domain; concrete implementations reside outside, keeping the domain independent.

Boundary Between Infrastructure and Domain Layers

The domain layer aggregates business concepts and must not depend on the infrastructure layer (just as a heart should not be affected by how food is obtained). Infrastructure provides the necessary channels for data, but the domain only cares about receiving the data.

In our example, the File entity has an owner field. When the infrastructure saves the file to the cloud, the File does not need to know whether the data ends up in a database or OpenSearch; such implementation details belong to the infrastructure layer.

For instance, a file forwarding use‑case can be expressed as:

Class File {
    Long id;
    String name;
    Long ownerId;
}
public void forwardFile(Long fileId, Long toUser) {
    File originalFile = FileFactory.createFile(fileId);
    File newFile = FileFactory.createFile(originalFile.getName(), toUser);
    Clouddisk clouddisk = ClouddiskFactory.createClouddisk(toUser);

    clouddisk.save(newFile);
}
Class FileFactory {
   public File createFile(Long fileId) {
       // May involve multiple repository queries and assembly
       return fileNodeRepository.getFileNode(fileId);
   }
}

Interfect FileNodeRepository {
    public File getFileNode(Long fromAliId);
}
class FileNodeRepositoryImpl impl FileNodeRepository {
    public File getFileNode(Long fromAliId) {
        Condition condition = creatCondition(fromAliId);
        Do obj = findInDB(condition);
        return covertDoToFile(obj);
    }
}

Thus the domain no longer has a strong dependency on the infrastructure.

"A business model should only depend on abstract interfaces it defines for external data; the concrete implementation details belong outside the domain."

Conclusion

This article does not exhaustively define DDD concepts such as anemic vs. rich models, aggregates, aggregate roots, or domain events. My personal takeaway is that DDD is overloaded with abstract concepts, and interpretations vary widely.

I believe the essence of DDD is business‑driven design; a good domain model should read like natural language and be understandable by non‑technical domain experts. Technical details belong outside the domain.

Click Follow the public account, “ Technical Nuggets ” for timely updates!

Javabackend architectureDomain-Driven DesignFactory PatternRepository Pattern
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login 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.