Why Your Java Base Controllers Are Misused and How to Refactor Them

The article examines common misuse of base Controller and Service classes in Java micro‑service projects, explains why they violate design principles such as Liskov Substitution, and provides concrete refactoring steps—including moving injections, constants, and utility methods to appropriate layers—to achieve a clean three‑tier architecture.

Programmer DD
Programmer DD
Programmer DD
Why Your Java Base Controllers Are Misused and How to Refactor Them

Charles Dickens wrote in *A Tale of Two Cities*: "It was the best of times, it was the worst of times." The rapid growth of mobile Internet creates many opportunities, but intensified competition makes many startups struggle. After years of experience in early‑stage companies, the author observed various Java micro‑service architectures, identified several problematic patterns, and proposes tentative solutions.

1. Use of Controller Base Class and Service Base Class

1.1 Phenomenon Description

1.1.1 Controller Base Class

Typical Controller base class:

<code/** Base controller class */
public class BaseController {
    /** Injected services */
    @Autowired
    protected UserService userService;
    /** Phone number pattern */
    protected static final String PHONE_PATTERN = "/^[1]([3-9])[0-9]{9}$/";
    /** Validate phone */
    protected static boolean validPhone(String phone) { ... }
}</code>

The base class mainly contains injected services, static constants and static helper methods so that all Controllers can inherit and directly use them.

1.1.2 Service Base Class

Typical Service base class:

<code/** Base service class */
public class BaseService {
    /** Injected DAO */
    @Autowired
    protected UserDAO userDAO;
    /** Injected services */
    @Autowired
    protected SmsService smsService;
    @Value("${example.systemName}")
    protected String systemName;
    /** Static constants */
    protected static final long SUPER_USER_ID = 0L;
    /** Service methods */
    protected UserDO getUser(Long userId) { ... }
    /** Static helper */
    protected static String getUserName(UserDO user) { ... }
}</code>

The Service base class aggregates DAO injection, service injection, configuration parameters, constants and helper methods for all Services.

1.2 Argument for Base Class Necessity

First, understand the Liskov Substitution Principle (LSP): any place that expects a base class must be able to use a subclass transparently.

Liskov Substitution Principle (LSP): All places that reference a base class must be able to use objects of its subclasses without knowing the difference.

Advantages of a proper base class:

Subclasses inherit all methods and fields, reducing duplication.

Improves code reuse.

Enhances extensibility; subclasses can add their own behavior.

However, the current Controller and Service base classes do not satisfy LSP because they are never directly substituted, lack abstract or virtual methods, and merely bundle resources that many subclasses do not need, causing unnecessary performance overhead.

1.3 Methods to Split Base Classes

1.3.1 Move Injection to Implementation Classes

According to the "use‑it‑or‑remove‑it" principle, inject only the DAO, services, or parameters that a concrete class actually needs.

<code/** User service class */
@Service
public class UserService {
    @Autowired
    private UserDAO userDAO;
    @Autowired
    private SmsService smsService;
    @Value("${example.systemName}")
    private String systemName;
    ...
}</code>

1.3.2 Move Static Constants to Constant Class

Encapsulate static constants in a dedicated constant class.

<code/** Example constants */
public class ExampleConstants {
    public static final long SUPER_USER_ID = 0L;
    ...
}</code>

1.3.3 Move Service Functions to Service Class

Place business functions in their respective Service classes and inject them where needed.

<code/** User service */
@Service
public class UserService {
    public UserDO getUser(Long userId) { ... }
    ...
}

/** Company service */
@Service
public class CompanyService {
    @Autowired
    private UserService userService;
    public UserDO getManager(Long companyId) {
        CompanyDO company = ...;
        return userService.getUser(company.getManagerId());
    }
    ...
}</code>

1.3.4 Move Static Functions to Utility Class

Static helper methods belong in a utility class.

<code/** User helper */
public class UserHelper {
    public static String getUserName(UserDO user) { ... }
    ...
}</code>

2. Put Business Code in Controller

2.1 Phenomenon Description

Typical Controller code directly performs business logic:

<code/** User controller */
@Controller
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserDAO userDAO;
    @ResponseBody
    @RequestMapping(path="/getUser", method=RequestMethod.GET)
    public Result<UserVO> getUser(@RequestParam Long userId) {
        UserDO userDO = userDAO.getUser(userId);
        if (userDO == null) return null;
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userDO, userVO);
        return Result.success(userVO);
    }
    ...
}</code>

The author of the code argued that a simple endpoint does not need a separate Service layer.

2.2 A Special Case

Example with @Value injection:

<code/** Test controller */
@Controller
@RequestMapping("/test")
public class TestController {
    @Value("${example.systemName}")
    private String systemName;
    @RequestMapping(path="/access", method=RequestMethod.GET)
    public String access() {
        return String.format("System(%s) welcomes you!", systemName);
    }
}</code>

Result: `系统(null)欢迎您访问!` because the property is not injected in the WebApplicationContext.

Note that actual processing of the @Value annotation is performed by a BeanPostProcessor. BeanPostProcessor interfaces are scoped per‑container, so a BeanPostProcessor defined in one container does not affect beans in another container.

Therefore, Controllers should not contain Service‑level logic.

2.3 Three‑Layer Architecture

Spring MVC follows a classic three‑tier architecture: Presentation (Controller), Business (Service), Persistence (Repository).

Putting business code in Controllers violates this structure.

3. Put Persistence Code in Service

3.1 Main Problems

Business and persistence logic are mixed, breaking the three‑tier separation.

Assembling SQL/HQL in business methods increases complexity.

Direct use of third‑party middleware hinders replacement.

Persistence code for the same entity is scattered across business methods.

Unit testing becomes difficult because persistence functions cannot be isolated.

3.2 Example: Hibernate Query in Service

<code/** User service */
@Service
public class UserService {
    @Autowired
    private SessionFactory sessionFactory;
    public UserVO getUserByEmpId(String empId) {
        String hql = "from t_user where emp_id = '" + empId + "'";
        Query query = sessionFactory.getCurrentSession().createQuery(hql);
        List<UserDO> list = query.list();
        if (list.isEmpty()) return null;
        UserVO vo = new UserVO();
        BeanUtils.copyProperties(list.get(0), vo);
        return vo;
    }
}</code>

Suggested refactoring: separate DAO and Service.

<code/** User DAO */
@Repository
public class UserDAO {
    @Autowired
    private SessionFactory sessionFactory;
    public UserDO getUserByEmpId(String empId) {
        String hql = "from t_user where emp_id = '" + empId + "'";
        Query query = sessionFactory.getCurrentSession().createQuery(hql);
        List<UserDO> list = query.list();
        return list.isEmpty() ? null : list.get(0);
    }
}

/** User service */
@Service
public class UserService {
    @Autowired
    private UserDAO userDAO;
    public UserVO getUserByEmpId(String empId) {
        UserDO userDO = userDAO.getUserByEmpId(empId);
        if (userDO == null) return null;
        UserVO vo = new UserVO();
        BeanUtils.copyProperties(userDO, vo);
        return vo;
    }
}</code>

About Code‑Generation Plugins

The Alibaba AliGenerator (based on MyBatis Generator) creates DAO code automatically. While convenient, it can introduce unnecessary boilerplate, obscure SQL, and cause accidental overwrites when the schema changes.

3.3 Redis Code in Service

<code/** User service */
@Service
public class UserService {
    @Autowired
    private UserDAO userDAO;
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    private static final String USER_KEY_PATTERN = "hash::user::%s";
    public void saveUser(UserVO user) {
        UserDO userDO = transUser(user);
        String key = MessageFormat.format(USER_KEY_PATTERN, userDO.getId());
        Map<String, String> map = new HashMap<>(8);
        map.put(UserDO.CONST_NAME, user.getName());
        map.put(UserDO.CONST_SEX, String.valueOf(user.getSex()));
        map.put(UserDO.CONST_AGE, String.valueOf(user.getAge()));
        redisTemplate.opsForHash().putAll(key, map);
        userDAO.save(userDO);
    }
}</code>

Refactor by extracting a Redis DAO:

<code/** User Redis DAO */
@Repository
public class UserRedis {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    private static final String KEY_PATTERN = "hash::user::%s";
    public void save(UserDO user) {
        String key = MessageFormat.format(KEY_PATTERN, user.getId());
        Map<String, String> map = new HashMap<>(8);
        map.put(UserDO.CONST_NAME, user.getName());
        map.put(UserDO.CONST_SEX, String.valueOf(user.getSex()));
        map.put(UserDO.CONST_AGE, String.valueOf(user.getAge()));
        redisTemplate.opsForHash().putAll(key, map);
    }
}

@Service
public class UserService {
    @Autowired
    private UserDAO userDAO;
    @Autowired
    private UserRedis userRedis;
    public void saveUser(UserVO user) {
        UserDO userDO = transUser(user);
        userRedis.save(userDO);
        userDAO.save(userDO);
    }
}</code>

This aligns Redis access with the three‑tier architecture.

4. Expose Database Model to Interface

4.1 Phenomenon Description

<code/** User DAO */
@Repository
public class UserDAO {
    public UserDO getUser(Long userId) { ... }
}

@Service
public class UserService {
    @Autowired
    private UserDAO userDAO;
    public UserDO getUser(Long userId) { return userDAO.getUser(userId); }
}

@Controller
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;
    @RequestMapping(path="/getUser", method=RequestMethod.GET)
    public Result<UserDO> getUser(@RequestParam Long userId) {
        UserDO user = userService.getUser(userId);
        return Result.success(user);
    }
}</code>

The controller returns the persistence entity `UserDO` directly, exposing internal table design, increasing payload size, risking sensitive data leakage, and coupling the API to the database schema.

4.2 Problems and Solutions

Direct exposure of database schema aids competitors.

Unrestricted fields waste bandwidth.

Sensitive fields may be unintentionally exposed.

Model‑to‑API mismatches cause maintenance headaches.

Lack of proper documentation reduces maintainability.

Solutions:

Enforce strict separation of persistence models and API models.

Structure projects to prevent accidental cross‑layer calls.

4.3 Three Project Structuring Approaches

1) Shared‑Model Project

All modules depend on a common `example-model` project that contains entity classes. This allows the web layer to call repository methods directly, which can bypass the service layer.

Risk: The presentation layer can invoke any service or DAO method, breaking the intended layering.

2) Model‑Separation Project

Introduce an `example-api` module that defines only API interfaces and VO classes. The service module implements these interfaces, while the web module depends solely on the API module.

Risk: The web layer can still reach internal service methods or DAOs, so organizational rules must forbid it.

3) Service‑Oriented Project

Business and repository modules are packaged as a Dubbo service (`example-dubbo`). Only the API defined in `example-api` is exposed, and other projects consume the service via RPC.

Explanation: Dubbo publishes only the API interfaces, guaranteeing that database entities never leak to callers.

4.4 A Not‑Recommended Suggestion

Some argue that using the same VO class in DAO methods speeds up development, but this violates the three‑tier isolation. The author presents an example where the DAO directly accepts a `QueryUserParameterVO` used by the API.

<code/** User DAO */
@Repository
public class UserDAO {
    public Long countByParameter(QueryUserParameterVO param) { ... }
    public List<UserVO> queryByParameter(QueryUserParameterVO param) { ... }
}

@Service
public class UserService {
    @Autowired
    private UserDAO userDAO;
    public PageData<UserVO> queryUser(QueryUserParameterVO param) {
        Long total = userDAO.countByParameter(param);
        List<UserVO> list = (total != null && total > 0) ? userDAO.queryByParameter(param) : null;
        return new PageData<>(total, list);
    }
}

@Controller
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;
    @RequestMapping(path="/queryUser", method=RequestMethod.POST)
    public Result<PageData<UserVO>> queryUser(@Valid @RequestBody QueryUserParameterVO param) {
        PageData<UserVO> page = userService.queryUser(param);
        return Result.success(page);
    }
}</code>

While this shortcut works, it compromises architectural purity and is therefore not recommended.

Afterword

Everyone has their own perspective; the content above reflects the author's personal experience in several startups. It is dedicated to those companies that allowed the author to refactor chaotic code and grow technically.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpring MVCService Layer
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

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.