Singleton in Spring Boot APIs

When building RESTful APIs with Spring Boot 3, developers often encounter unexpected data overlap issues, where multiple clients retrieve each other’s values. This issue frequently arises due to the singleton pattern in Java, which can unintentionally share data across client requests. This article explores why this happens and provides practical solutions to prevent client data overlap, ensuring a more reliable and isolated RESTful API experience.

REST API troubleshooting

Developers new to Java applications may need clarification when multiple clients swap or retrieve each other’s values.

When an application doesn’t work, the first question on a developer’s mind is usually, “What went wrong?”.

Introducing an instance class called singleton in the design pattern causes the problem.

Singleton is a design pattern that allows only one class instance in the JVM. It reduces logical memory.

Understanding Singleton Pattern Issues in Spring Boot 3

This example will demonstrate the concept of the singleton.

import com.example.demo.services.UserInfoServiceInf;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserInfo {

    public UserInfoServiceInf userInfoServiceInf;

    @Autowired
    public UserInfo(UserInfoServiceInf userInfoServiceInf){
        this.userInfoServiceInf = userInfoServiceInf;
    }


    @GetMapping(path = "/userinfo/{id}", produces = "application/json")
    public String getEncrypt(@PathVariable String id) {
        System.out.println("id="+id);
        return userInfoServiceInf.getUserName(id);
    }

}
@Service
public class UserInfoServiceImpl implements UserInfoServiceInf{

    private String username;

    public String getUserName(String userId){
        if(username == null) username = "";
        if(userId.equals("1")){
            username = "demoUser";
        }
        System.out.println("username = "+username);
        System.out.println("hashCode = "+username.hashCode());
        return username;
    }

}
public interface  UserInfoServiceInf {
    public String getUserName(String userId);
}

1. “When Client 1 sends a request to the server.”

curl http://localhost:8080/userinfo/1
demoUser

2. In the output console

id=1
username = demoUser
hashCode = 856667214

3. Then, Client 2 sends a request to the server.

curl http://localhost:8080/userinfo/2
demoUser

4. In the output console

id=2
username = demoUser
hashCode = 856667214

Why does client 2 receive the same response value as client one from the server instead of a blank value?
This is because the JVM uses the instance class for every request.

How does the developer know that the class is the instance class?
How does the developer know which class the instance class is in on the JVM?

In the example, both requests have the same hash code from the variable “username”.

The conclusion is that JVM assigns an instance class to every request.

To prevent this problem, move the global variable to a local variable. The instance class shares the value of the global variable.

Solutions for Avoiding Shared Data in RESTful APIs

For example, move “username” from the global variable to a local variable in the UserInfoServiceImpl class.

@Service
public class UserInfoServiceImpl implements UserInfoServiceInf{
    
    public String getUserName(String userId){
        String username = "";
        if(userId.equals("1")){
            username = "demoUser_local";
        }
        System.out.println("username = "+username);
        System.out.println("hashCode = "+username.hashCode());
        return username;
    }

}

1. “When Client 1 sends a request to the server.”

curl http://localhost:8080/userinfo/1
demoUser

2. In the output console

id=1
username = demoUser_local
hashCode = 333513786

3. Then, Client 2 sends a request to the server.

curl http://localhost:8080/userinfo/2

4. In the output console

id=2
username = 
hashCode = 0

The output console displays two requests to retrieve the variable username’s hash code value.

Prototype scope

In some cases or for specific business requirements, global variables may need to be declared. Developers can specify this in the prototype scope.

@RestController
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class UserInfo {

    public UserInfoServiceInf userInfoServiceInf;

    @Autowired
    public UserInfo(UserInfoServiceInf userInfoServiceInf){
        this.userInfoServiceInf = userInfoServiceInf;
    }


    @GetMapping(path = "/userinfo/{id}", produces = "application/json")
    public String getEncrypt(@PathVariable String id) {
        System.out.println("id="+id);
        return userInfoServiceInf.getUserName(id);
    }

}
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class UserInfoServiceImpl implements UserInfoServiceInf{

    private String username;
    
    public String getUserName(String userId){
        if(username == null) username = "";
        if(userId.equals("1")){
            username = "demoUser_local";
        }
        System.out.println("username = "+username);
        System.out.println("hashCode = "+username.hashCode());
        return username;
    }

Each request creates a new class instance in Java memory, potentially causing memory leaks, out-of-memory errors or gabage object if not handled properly.

Finally

The Singleton concept helps manage the JVM’s resources and prevent memory leaks and out-of-memory issues in certain situations. The JVM can handle more concurrent processes than usual. Developers need to understand this concept, as it can benefit them.

This article was originally published on Medium.

Leave a Comment

Your email address will not be published. Required fields are marked *