Designing a Monolithic Application for Future Microservice Migration: Best Practices and Code Structure
The article explains how to design a monolithic Java application with clear business boundaries, modular code organization, MVC layering, and disciplined controller logic so that it can be smoothly refactored into a micro‑service architecture, illustrating the approach with directory trees, package schemes, and sample CommonResult code.
With the rapid growth of mobile and B2C models, many companies have experienced explosive business growth, prompting a shift toward microservice architectures to handle high traffic, concurrency, and stability requirements.
However, a microservice architecture is rarely adopted from the start; most projects begin with a monolithic design to quickly deliver features and capture market share, then evolve iteratively.
The author shares personal experience transitioning from monolith to microservices, highlighting the contrast between well‑designed and poorly designed architectures. Good designs feature elegant code structure, clear layering, well‑defined business boundaries, and clear responsibilities for developers, while bad designs lead to tangled code, difficult maintenance, and constant patching.
Define Clear Business Boundaries
When planning for future microservices, the monolith should already separate domains such as user, product, order, payment, and permission. Domain‑Driven Design (DDD) can guide this separation, though it requires skilled practitioners.
Code Layer Structure
Following the MVC pattern, the recommended module layout is:
├─demo-common
│ └─src/main/java & resources
├─demo-dao
│ └─src/main/java & resources
├─demo-service
│ └─src/main/java & resources
└─demo-web
└─src/main/java & resourcesThe four modules serve the following purposes:
demo-common: basic utilities, constants, configuration.
demo-dao: DAO layer, mapper interfaces and XML files.
demo-service: service interfaces and business logic.
demo-web: web layer, controllers, and external API exposure.
Dependencies flow from web → service → dao → common.
Package Organization
Two common schemes exist: (1) organize packages by business domain, each containing its own MVC layers; (2) organize first by MVC layers (service, serviceImpl, dto, etc.) and then place business models inside. The author prefers the domain‑first approach because it aligns well with later microservice extraction—each business package can be copied into its own service with minimal changes.
Example package names:
com.example.jajian.service.order,
com.example.jajian.service.userFull source tree example:
└─src
├─main
│ ├─java
│ │ └─com
│ │ └─example
│ │ └─jajian
│ │ ├─common
│ │ └─service
│ │ ├─order
│ │ │ ├─dto/OrderDto.java
│ │ │ └─service/OrderService.java
│ │ ├─pay
│ │ │ ├─dto/PayDto.java
│ │ │ └─service/PayService.java
│ │ └─user
│ │ ├─dto/UserDto.java
│ │ └─service/UserService.javaAvoid Cross‑Domain Multi‑Table Joins
While multi‑table joins are convenient in a monolith, they become problematic after services are split because tables may reside in different databases accessed via RPC, making such joins impossible.
Keep Business Logic Out of Controllers
Controllers should only handle request/response conversion and delegate validation and business processing to the service layer. Global exception handlers can manage error handling uniformly.
Data Transfer Objects
DO (Data Object): maps directly to database tables, used by the DAO layer.
DTO (Data Transfer Object): carries data between service and client layers.
VO (View Object): used by the presentation layer, often for UI rendering.
Separating these objects enables flexible handling of concerns such as data masking or ID encryption.
Unified Response Wrapper
A generic CommonResult<T> class is provided to standardize API responses, with success and failure codes, data payload, and message fields.
public class CommonResult
{
public static final String CODE_SUCCESS = "0";
public static final String CODE_FAILED = "9999";
private String code;
private T data;
private String msg;
private CommonResult(String code, T data, String msg) { ... }
public boolean isSuccess() { return CODE_SUCCESS.equals(code); }
public static
CommonResult
success() { return new CommonResult<>(CODE_SUCCESS, null, null); }
public static
CommonResult
success(T data) { return new CommonResult<>(CODE_SUCCESS, data, null); }
public static
CommonResult
success(T data, String msg) { return new CommonResult<>(CODE_SUCCESS, data, msg); }
public static
CommonResult
failed() { return new CommonResult<>(CODE_FAILED, null, null); }
public static
CommonResult
failed(String errorCode, String msg) { return new CommonResult<>(errorCode, null, msg); }
// setters/getters omitted
}Conclusion
The article lists practical considerations for designing a monolithic system that can later be split into microservices, emphasizing clear domain boundaries, modular package structures, minimal controller logic, and standardized data objects to reduce refactoring effort and avoid common pitfalls.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.