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.
❝ 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!
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.