Backend Development 14 min read

Applying Domain-Driven Design and Modular Architecture in Backend Services

This article introduces a modular backend service architecture based on Domain-Driven Design, explaining project structure, module composition, hexagonal architecture, and practical code examples using Spring Cloud, illustrating how to achieve flexible, maintainable services through domain models, adapters, and proper boundary definitions.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Applying Domain-Driven Design and Modular Architecture in Backend Services

This article is the first signed post in the Juejin technology community, with a 14‑day exclusive period before redistribution is allowed.

The column will build a modular backend service that can be combined or extended according to project functional needs and scale.

Project structure and modular construction ideas RESTful API design & management Gateway routing modular support and conditional configuration DDD domain‑driven design and business modularization (concepts and understanding) DDD domain‑driven design and business modularization (implementation) DDD domain‑driven design and business modularization (Schrödinger model) DDD domain‑driven design and business modularization (optimization and refactoring) RPC modular design and distributed transactions Event modular design and lifecycle logging

In a previous article we used part of Juejin's functionality to build a modular backend project consisting of three modules: juejin , juejin-user (user), juejin-pin (pin), and juejin-message (message).

By adding startup modules, any combination and extension of functional modules is possible.

Example 1: Use the startup module juejin-appliaction-system to merge juejin-user and juejin-message into a single service, reducing server resource consumption. Then use juejin-appliaction-pin to provide the juejin-pin module as a separate service for precise scaling of high‑traffic features.

Example 2: Use the startup module juejin-appliaction-single to package juejin-user , juejin-message , and juejin-pin into a monolithic application, suitable for early‑stage projects with small scale.

PS: The examples are based on IDEA + Spring Cloud .

DDD

DDD stands for Domain‑Driven Design , a guiding philosophy for modeling complex business domains.

The author first encountered DDD two years ago, found many articles full of jargon, and decided to read a book to gain a solid understanding.

Although reading the book does not make you master DDD, applying its core ideas to a project can dramatically improve quality.

DDD mirrors the real world; many concepts are abstractions of everyday life.

How to Apply

First, remember the purpose of using DDD, such as improving maintainability or clarifying complex business logic.

Then, fully understand each definition and the problems it solves.

Finally, ask whether your current project has those problems and whether DDD can address them.

Avoid formalism: merely copying examples without solving real issues can make the system more complex.

The author applied DDD with some adaptations to fit personal development habits.

Domain

DDD defines domain , sub‑domain , and bounded context to isolate modules and prevent coupling.

For example, the juejin-pin module may need user information from juejin-user . A naive approach would directly call userFeign.getUserById(id) , causing tight coupling.

public Map
getUser(String id) {
    return userFeign.getUserById(id);
}

If the user module changes its data structure, all callers break.

Instead, define a specific PinUser type for the pin module and use an anti‑corruption layer to convert external user data.

public class FeignPinUserProvider implements PinUserProvider {
    // ...
    @Override
    public PinUser getUser(String id) {
        return toPinUser(userFeign.getUserById(id));
    }

    public PinUser toPinUser(Map
user) {
        // conversion logic
    }
}

This isolates the pin module from changes in the user module; only the conversion method needs updating.

Hexagonal Architecture

Also known as the Ports and Adapters architecture. Using the pin module as an example, the core is the domain model and domain services.

Adapters can be added for HTTP, WebSocket, MySQL, Redis, or any other technology without affecting the domain logic.

Switching a persistence technology (e.g., MySQL to Oracle) only requires a new adapter, leaving the domain untouched.

Compared with MVC, which centers on the data model, hexagonal architecture centers on the domain model, making the system more resilient to changes in data storage or external interfaces.

Domain Model

Domain models emphasize behavior, unlike data models that only hold data. Concepts include entities, value objects, anemic vs. rich models.

Example JSON representations of a “pin” (post), its comments, and replies are shown for both data‑model and domain‑model perspectives.

// Data model example (simplified)
{
    "id": "Pin ID",
    "content": "Pin content",
    "clubId": "Club ID",
    "userId": "User ID",
    "createTime": "Timestamp"
}

// Domain model example (rich structure)
{
    "id": "Pin ID",
    "content": "Pin content",
    "club": {"id": "Club ID", "name": "Club name", "logo": "...", "description": "..."},
    "user": {"id": "User ID", "name": "User name", "profilePicture": "..."},
    "comments": [
        {
            "id": "Comment ID",
            "comment": "Comment text",
            "user": {"id": "...", "name": "...", "profilePicture": "..."},
            "comments": [
                {
                    "id": "Reply ID",
                    "comment": "Reply text",
                    "user": {"id": "...", "name": "...", "profilePicture": "..."},
                    "comments": [],
                    "likes": [],
                    "createTime": "..."
                }
            ],
            "likes": [],
            "createTime": "..."
        }
    ],
    "likes": [],
    "createTime": "..."
}

Domain‑driven updates are expressed as behavior on the model:

// Add a comment
Pin pin = getPin(id);
pin.addComment(comment);

// Reply to a comment
Pin pin = getPin(id);
Comment comment = pin.getComment(commentId);
comment.addComment(reply);

// Reply to a reply
Pin pin = getPin(id);
Comment comment = pin.getComment(commentId);
Comment reply = comment.getComment(replyId);
reply.addComment(newComment);

The key idea is “let the domain model do its own work”, keeping business logic inside the model rather than scattered across services.

Conclusion

Domain models provide a visual representation that aligns closely with real‑world concepts, making it easier for product, design, and testing teams to understand.

Because the domain model reflects stable business concepts, it rarely needs changes unless major requirements shift, reducing the risk of redesign compared to traditional code‑centric approaches. This stability is the greatest advantage of DDD.

backend architectureDDDSpring Cloudhexagonal architecturemodular design
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.