Managing the wizard UI component by the strategy design pattern in spring boot backend service

Serdar A.
11 min readJul 5, 2023

--

Hello everyone

For Turkish : https://lastjavabuilder.medium.com/spring-boot-mikroservisinde-strateji-tasar%C4%B1m-deseni-ile-wizard-ui-bile%C5%9Feni-y%C3%B6netmek-aaf1fd0b9955

Whether you have a monolithic application or a microservice architecture application , if you have a wizard component in your frontend application and you want to manage it on the backend with a small number of endpoints, I would like to show an example application on the backend side in this article about how to do this with the strategy design pattern.

My opinion or my code can of course be improved, but I would like to share it with you here to get an idea.

Lets start :)

Let’s say we have a wizard component like the following in our frontend application. Sorry for the drawings :)

  • Basic information entry screen for the product on the 1st screen
  • Detailed information entry screen for the product on the 2nd screen
  • On the 3rd screen, we can enter the specifications for the product.
  • On the 4th screen, the login screen for the category for the product
  • On the 5th screen, the login screen for the company information for the product

Instead of writing get, post, put etc. apis for each screen separately, we can use the strategy design pattern to do it in a shorter way.In this application, we will define 1 get api and 1 post api. We will try to manage our screens with these 2 APIs.

So let’s start writing our backend service.

First of all, in our spring boot application, we create 1 enum class to determine which screen we will manage for each screen in the wizard.

WizardSceenEnum

public enum WizardSceenEnum {

PRODUCT_INFO_SCREEN(1,"ProductInfo","Entering infos of product base"),
PRODUCT_DETAIL_SCREEN(2,"ProductDetail","Entering info of product details"),
PRODUCT_CATEGORY_SCREEN(3,"ProductCategory","Entering category info of product"),
PRODUCT_COMPANY_SCREEN(4,"ProductCompany","Entering company info of product"),
PRODUCT_SPECIFICATION_SCREEN(5,"ProductSpecification","Entering specifications of product");

private Integer order;
private String screenKey;
private String screenDescription;

WizardSceenEnum(Integer order, String screenKey, String screenDescription) {
this.order = order;
this.screenKey = screenKey;
this.screenDescription = screenDescription;
}

WizardSceenEnum findScreenByOrderId(Integer orderIndex)
{
for(WizardSceenEnum wizardSceenEnum : values())
{
if(wizardSceenEnum.getOrder().equals(orderIndex))
{
return wizardSceenEnum;
}
}

return null;
}

WizardSceenEnum findScreenByKey(String key)
{
for(WizardSceenEnum wizardSceenEnum : values())
{
if(wizardSceenEnum.getScreenKey().equals(key))
{
return wizardSceenEnum;
}
}

return null;
}

public Integer getOrder() {
return order;
}

public String getScreenKey() {
return screenKey;
}

public String getScreenDescription() {
return screenDescription;
}
}

Each value in the enum corresponds to each screen

  • order : display order for screens
  • screenKey : key for screens
  • screenDescription : description for screens

Again, we defined an enum to define whether the request from the api is create or update.

WizardScreenRequestTypeEnum

public enum WizardScreenRequestTypeEnum {

CREATE("CREATE"),
UPDATE("UPDATE");

private String requestType;

WizardScreenRequestTypeEnum( String requestType) {
this.requestType = requestType;
}

WizardScreenRequestTypeEnum getScreenRequestTypeByKey(String key)
{
for (WizardScreenRequestTypeEnum wizardScreenRequestTypeEnum: values())
{
if(wizardScreenRequestTypeEnum.getRequestType().equals(key))
{
return wizardScreenRequestTypeEnum;
}
}

return null;
}

public String getRequestType() {
return requestType;
}
}

We define an enum like this to indicate whether the request thrown by the frontend is a post request or a put request.Actually, in this part, I also defined such an enum value to use a single post method not to define a put method.

It’s time to define the DTOs required to manage each screen.

First of all, I define BaseDTO.


@Data
public class BaseDTO implements Serializable {

private Long id;
}

I then start defining DTOs for my displays.

ProductDTO

@Data
@EqualsAndHashCode(callSuper = false)
public class ProductDTO extends BaseDTO {

private String productName;
private String productCode;
private String productTitle;
private String productDetail;
}

I can use the same dto for the product detail screen.

SpecificationDTO

@Data
@EqualsAndHashCode(callSuper = false)
public class SpecificationDTO extends BaseDTO {

private String description;
}

CategoryDTO

@Data
@EqualsAndHashCode(callSuper = false)
public class CategoryDTO extends BaseDTO {

private String categoryName;
}

CompanyDTO

@Data
@EqualsAndHashCode(callSuper = false)
public class CompanyDTO extends BaseDTO {

private String companyName;
}

We need to define one more DTO that wraps all these DTOs. This DTO will be returned for get endpoint and post endpoint.

WizardScreenDTO

@Data
@EqualsAndHashCode(callSuper = false)
public class WizardScreenDTO extends BaseDTO {

private ProductDTO productDTO;
private CategoryDTO categoryDTO;
private CompanyDTO companyDTO;
private List<SpecificationDTO> specificationDTOList = new LinkedList<>();

}

And finally we need to define a DTO. The purpose of this DTO is that we will make use of this DTO when making post request.

WizardRequestDTO

@Data
public class WizardRequestDTO implements Serializable {

private WizardSceenEnum sceenEnum;
private WizardScreenRequestTypeEnum requestTypeEnum;
private WizardScreenDTO wizardScreenDTO;
}
  • sceenEnum : It specifies which screen we will manage.
  • requestTypeEnum : Indicates whether the request is a create or update.
  • wizardScreenDTO : DTO that wraps the DTO that contains the information of the related screen

Now we need 1 interface that will be implemented by the classes that manage all these screens separately.

IWizardScreenService

public interface IWizardScreenService {

<T extends BaseDTO> T getWizard(Long id);

<T extends BaseDTO> T createModelWizard(WizardScreenDTO createdWizardScreenDTO);

<T extends BaseDTO> T updateModelWizard(WizardScreenDTO updateddWizardScreenDTO);

WizardSceenEnum getScreenName();
}
  • getWizard : Whatever the id parameter is (productId, companyId, categoryId or access child tables via productId) , it is a method that returns information from the database or another related service.
  • createModelWizard : Which screen will save operations will be done with this method. This method takes wrap class parameter of type WizardScreenDTO. In this Wrap class, there are DTOs for the screen we want.
  • updateModelWizard : Which screen will be updated with this method will be done.This method takes wrap class parameter of type WizardScreenDTO. In this Wrap class, there are DTOs for the screen we want.
  • getScreenName : The method that returns the information of the screen related to the screen for which the operation is performed.

In this class, I have made the return types of the methods generic, but you can do it differently. This is actually one of my purposes for using BaseDTO.

With this interface, we will create classes that manage our screens and handle their own logic separately.But first of all, we need to ensure which screen is handled according to our request in the runtime. For this we need a factory class.

WizardFactoryService

@Service
public class WizardFactoryService {

private Map<WizardSceenEnum, IWizardScreenService> wizardScreens;

@Autowired
public WizardFactoryService(Set<IWizardScreenService> IWizardScreenServiceSet)
{
createScreenWizard(IWizardScreenServiceSet);
}

public IWizardScreenService getScreenWizard(WizardSceenEnum wizardSceenEnum)
{
return wizardScreens.get(wizardSceenEnum);
}

private void createScreenWizard(Set<IWizardScreenService> IWizardScreenServiceSet) {

wizardScreens = new HashMap<>();
IWizardScreenServiceSet.forEach(projectWizardService -> wizardScreens.put(projectWizardService.getScreenName(),projectWizardService));
}
}

What this factory class actually does is it will create the class that manages the screen associated with this enum value and return it to us, according to the wizardSceenEnum parameter we sent.

Now we can start writing our screen classes, which have their own separate logic section that manages each screen.

ProductInfoScreenService

@Service
public class ProductInfoScreenService implements IWizardScreenService {

@Override
public <T extends BaseDTO> T getWizard(Long id) {

//id -> product table id
//get info from database or get another microservice

WizardScreenDTO wizardScreenDTO = new WizardScreenDTO();
ProductDTO productDTO = createProductDTO(id);

wizardScreenDTO.setProductDTO(productDTO);

return (T) wizardScreenDTO;
}

@Override
public <T extends BaseDTO> T createModelWizard(WizardScreenDTO createdProjectDTO) {

ProductDTO productDTO = createdProjectDTO.getProductDTO();

//save dto for entity or send another service get response

WizardScreenDTO wizardScreenDTO = new WizardScreenDTO();
ProductDTO createdProductDTO = saveProductDTO(productDTO);

wizardScreenDTO.setProductDTO(createdProductDTO);

return (T) wizardScreenDTO;
}

@Override
public <T extends BaseDTO> T updateModelWizard(WizardScreenDTO updatedProjectDTO) {

ProductDTO productDTO = updatedProjectDTO.getProductDTO();

WizardScreenDTO wizardScreenDTO = new WizardScreenDTO();
ProductDTO updatedProductDTO = updateProductDTO(productDTO);

wizardScreenDTO.setProductDTO(updatedProductDTO);
return (T) wizardScreenDTO;
}

@Override
public WizardSceenEnum getScreenName() {
return WizardSceenEnum.PRODUCT_INFO_SCREEN;
}

private ProductDTO createProductDTO(Long id) {

ProductDTO productDTO = new ProductDTO();
productDTO.setId(id);
productDTO.setProductName("product 1");
productDTO.setProductCode("p1");
productDTO.setProductTitle("product1 title");

return productDTO;
}

private ProductDTO saveProductDTO(ProductDTO productDTO) {

ProductDTO createdDTO = new ProductDTO();
createdDTO.setProductName(productDTO.getProductName());
createdDTO.setProductCode(productDTO.getProductCode());
createdDTO.setProductTitle(productDTO.getProductTitle());

//save database
createdDTO.setId(2l);
return createdDTO;
}

private ProductDTO updateProductDTO(ProductDTO productDTO) {

//update database
return productDTO;
}
}

This class impelement IWizardScreenService.

In this class, you can perform database operations as well as send a request to another microservice and perform various operations. I kept the logic part simple as an example. In this class, I made logic operations for the input values on the first screen.

For example, you can write a smaller query and pull the minimum information required for this screen from the database accordingly.

ProductDetailScreenService

@Service
public class ProductDetailScreenService implements IWizardScreenService {

@Override
public <T extends BaseDTO> T getWizard(Long id) {

//id -> product table id
//get info from database or get from another microservice

WizardScreenDTO wizardScreenDTO = new WizardScreenDTO();
ProductDTO productDetailDTO = createProductDetailDTO(id);
wizardScreenDTO.setProductDTO(productDetailDTO);

return (T) wizardScreenDTO;
}

@Override
public <T extends BaseDTO> T createModelWizard(WizardScreenDTO createdProjectDetailDTO) {

ProductDTO productDTO = createdProjectDetailDTO.getProductDTO();

//save database or send another service get response

WizardScreenDTO wizardScreenDTO = new WizardScreenDTO();
ProductDTO createdProductDetailDTO = saveProductDetailDTO(productDTO);

wizardScreenDTO.setProductDTO(createdProductDetailDTO);

return (T) wizardScreenDTO;
}

@Override
public <T extends BaseDTO> T updateModelWizard(WizardScreenDTO updatedProjectDTO) {

ProductDTO productDTO = updatedProjectDTO.getProductDTO();

//save database or send another service

WizardScreenDTO wizardScreenDTO = new WizardScreenDTO();
ProductDTO updatedProductDetailDTO = updateProductDetailDTO(productDTO);

wizardScreenDTO.setProductDTO(updatedProductDetailDTO);

return (T) wizardScreenDTO;
}

@Override
public WizardSceenEnum getScreenName() {
return WizardSceenEnum.PRODUCT_DETAIL_SCREEN;
}

private ProductDTO createProductDetailDTO(Long id) {

ProductDTO productDTO = new ProductDTO();
productDTO.setId(id);
productDTO.setProductDetail("product 1 detail info ");

return productDTO;

}

private ProductDTO saveProductDetailDTO(ProductDTO productDTO) {

Long id = 1l; // get productId
ProductDTO createdDTO = new ProductDTO();
createdDTO.setId(id);
createdDTO.setProductDetail(productDTO.getProductDetail());

//save database
return createdDTO;

}

private ProductDTO updateProductDetailDTO(ProductDTO productDTO) {

//update database
return productDTO;
}
}

This class also performs operations that manage only the description information, not the other information of the Product object as logic.

ProductSpecificationScreenService

@Service
public class ProductSpecificationScreenService implements IWizardScreenService {

@Override
public <T extends BaseDTO> T getWizard(Long id) {

//id -> product table id
//get info from database with or another microservice

WizardScreenDTO wizardScreenDTO = new WizardScreenDTO();
List<SpecificationDTO> specificationDTOList = createProductSpecificationList();
wizardScreenDTO.setSpecificationDTOList(specificationDTOList);

return (T) wizardScreenDTO;
}

@Override
public <T extends BaseDTO> T createModelWizard(WizardScreenDTO createdProjectSpecDTO) {

ProductDTO productDTO = createdProjectSpecDTO.getProductDTO();

List<SpecificationDTO> createSpecificationDTOList = createdProjectSpecDTO.getSpecificationDTOList();

AtomicLong counter = new AtomicLong(100);

createSpecificationDTOList.forEach(specificationDTO -> {
specificationDTO.setId(counter.getAndIncrement());
});

//save product specification table with createSpecificationDTOList

WizardScreenDTO wizardScreenDTO = new WizardScreenDTO();
wizardScreenDTO.setProductDTO(productDTO);
wizardScreenDTO.setSpecificationDTOList(createSpecificationDTOList);

return (T) wizardScreenDTO;
}

@Override
public <T extends BaseDTO> T updateModelWizard(WizardScreenDTO updatedProjectSpecDTO) {

ProductDTO productDTO = updatedProjectSpecDTO.getProductDTO();

List<SpecificationDTO> updateSpecificationDTOList = updatedProjectSpecDTO.getSpecificationDTOList();

//update product specification table with updateSpecificationDTOList

updateSpecificationDTOList.forEach(specificationDTO -> {

specificationDTO.setDescription(specificationDTO + " updated");
});

WizardScreenDTO wizardScreenDTO = new WizardScreenDTO();
wizardScreenDTO.setProductDTO(productDTO);
wizardScreenDTO.setSpecificationDTOList(updateSpecificationDTOList);

return (T) wizardScreenDTO;
}

@Override
public WizardSceenEnum getScreenName() {
return WizardSceenEnum.PRODUCT_SPECIFICATION_SCREEN;
}

private List<SpecificationDTO> createProductSpecificationList() {

List<SpecificationDTO> list = new ArrayList<>();

SpecificationDTO dto1 = createSpecificationDTO(300l,"spec 1");
SpecificationDTO dto2 = createSpecificationDTO(400l,"spec 2");

list.add(dto1);
list.add(dto2);

return list;

}

private SpecificationDTO createSpecificationDTO(Long specId, String specName) {

SpecificationDTO dto = new SpecificationDTO();
dto.setId(specId);
dto.setDescription(specName);

return dto;
}
}

This class also performs operations that only manage the specification information, not the other information of the product object.

ProductCategoryScreenService

@Service
public class ProductCategoryScreenService implements IWizardScreenService {
@Override
public <T extends BaseDTO> T getWizard(Long id) {

//id -> product table id or category id
//get info from database or get another microservice

WizardScreenDTO wizardScreenDTO = new WizardScreenDTO();
CategoryDTO categoryDTO = new CategoryDTO();
categoryDTO.setId(2l);
categoryDTO.setCategoryName("Category 1");

wizardScreenDTO.setCategoryDTO(categoryDTO);

return (T) wizardScreenDTO;
}

@Override
public <T extends BaseDTO> T createModelWizard(WizardScreenDTO createdCategoryDTO) {

CategoryDTO createCategory = createdCategoryDTO.getCategoryDTO();
ProductDTO productDTO = createdCategoryDTO.getProductDTO();

// save product table with category in database or send another service get response

WizardScreenDTO wizardScreenDTO = new WizardScreenDTO();
wizardScreenDTO.setProductDTO(productDTO);
wizardScreenDTO.setCategoryDTO(createCategory);

return (T) wizardScreenDTO;
}

@Override
public <T extends BaseDTO> T updateModelWizard(WizardScreenDTO updatedCategoryDTO) {

CategoryDTO updateCategory = updatedCategoryDTO.getCategoryDTO();
ProductDTO productDTO = updatedCategoryDTO.getProductDTO();

//update product with category in database or send another service get response
WizardScreenDTO wizardScreenDTO = new WizardScreenDTO();
wizardScreenDTO.setProductDTO(productDTO);
wizardScreenDTO.setCategoryDTO(updateCategory);

return (T) wizardScreenDTO;
}

@Override
public WizardSceenEnum getScreenName() {
return WizardSceenEnum.PRODUCT_CATEGORY_SCREEN;
}
}

ProductCompanyScreenService

@Service
public class ProductCompanyScreenService implements IWizardScreenService {

@Override
public <T extends BaseDTO> T getWizard(Long id) {

//id -> product table id or company id
//get info from database or get another microservice

WizardScreenDTO wizardScreenDTO = new WizardScreenDTO();
CompanyDTO companyDTO = new CompanyDTO();
companyDTO.setId(100l);
companyDTO.setCompanyName("Company 100");

wizardScreenDTO.setCompanyDTO(companyDTO);

return (T) wizardScreenDTO;
}

@Override
public <T extends BaseDTO> T createModelWizard(WizardScreenDTO createdCompanyDTO) {

CompanyDTO createCompany = createdCompanyDTO.getCompanyDTO();
ProductDTO productDTO = createdCompanyDTO.getProductDTO();

// save product table with category in database or send another service get response

WizardScreenDTO wizardScreenDTO = new WizardScreenDTO();
wizardScreenDTO.setProductDTO(productDTO);
wizardScreenDTO.setCompanyDTO(createCompany);

return (T) wizardScreenDTO;
}

@Override
public <T extends BaseDTO> T updateModelWizard(WizardScreenDTO updatedCompanyDTO) {

CompanyDTO updateCompany= updatedCompanyDTO.getCompanyDTO();
ProductDTO productDTO = updatedCompanyDTO.getProductDTO();

//update product with category in database or send another service get response
WizardScreenDTO wizardScreenDTO = new WizardScreenDTO();
wizardScreenDTO.setProductDTO(productDTO);
wizardScreenDTO.setCompanyDTO(updateCompany);

return (T) wizardScreenDTO;
}

@Override
public WizardSceenEnum getScreenName() {
return WizardSceenEnum.PRODUCT_COMPANY_SCREEN;
}
}

If you notice in these classes, I return the enum value for each screen in the getScreenName() method.

After doing all this, we can now write the Controller and our Service class that manages this Controller.

We need an interface for service class

IWizardService

public interface IWizardService {

ResponseEntity<WizardScreenDTO> postWizard(WizardRequestDTO wizardRequestDTO);

ResponseEntity<WizardScreenDTO> getWizard(Long id, WizardSceenEnum wizardSceenEnum);
}

Before say, I have two endpoints and two service methods

WizardService

@Service
public class WizardService implements IWizardService {

private final WizardFactoryService wizardFactoryService;

@Autowired
public WizardService(WizardFactoryService wizardFactoryService) {
this.wizardFactoryService = wizardFactoryService;
}

@Override
public ResponseEntity<WizardScreenDTO> postWizard(WizardRequestDTO wizardRequestDTO) {

IWizardScreenService wizardScreenService = wizardFactoryService.getScreenWizard(wizardRequestDTO.getSceenEnum());

if(wizardRequestDTO.getRequestTypeEnum().getRequestType().equals(WizardScreenRequestTypeEnum.CREATE.getRequestType()))
{
/**
* create
*/

WizardScreenDTO createdDTO = wizardScreenService.createModelWizard(wizardRequestDTO.getWizardScreenDTO());
return new ResponseEntity<>(createdDTO, HttpStatus.CREATED);
}
else if(wizardRequestDTO.getRequestTypeEnum().getRequestType().equals(WizardScreenRequestTypeEnum.UPDATE.getRequestType()))
{
/**
* update
*/
WizardScreenDTO updatedDTO = wizardScreenService.updateModelWizard(wizardRequestDTO.getWizardScreenDTO());
return new ResponseEntity<>(updatedDTO, HttpStatus.OK);
}

return new ResponseEntity<>(new WizardScreenDTO(), HttpStatus.NOT_ACCEPTABLE);
}

@Override
public ResponseEntity<WizardScreenDTO> getWizard(Long id, WizardSceenEnum wizardSceenEnum) {

if(Objects.nonNull(id))
{
IWizardScreenService wizardScreenService = wizardFactoryService.getScreenWizard(wizardSceenEnum);
WizardScreenDTO wizardScreenDTO = wizardScreenService.getWizard(id);

return new ResponseEntity<>(wizardScreenDTO,HttpStatus.OK);
}
return new ResponseEntity<>(new WizardScreenDTO(), HttpStatus.BAD_REQUEST);
}
}

The line to pay attention to when we look inside our methods :

IWizardScreenService wizardScreenService =  wizardFactoryService.getScreenWizard(wizardRequestDTO.getSceenEnum());

According to the enum parameter that our factory class receives in the getScreenWizard method, it actually returns our class that manages the relevant screen as IWizardScreenService.

For example :

  • enum value : PRODUCT_INFO_SCREEN return class : ProductInfoScreenService
  • enum value : PRODUCT_COMPANY_SCREEN return class : ProductCompanyScreenService

After that, when we call getWizard(), createModelWizard(), updateModelWizard(), the logic in the related methods of the returning class will be executed.

We finally write Controller class

WizardScreenController

@RestController
@RequestMapping("wizard-controller")
public class WizardScreenController {

private final IWizardService wizardService;

@Autowired
public WizardScreenController(IWizardService wizardService) {
this.wizardService = wizardService;
}

@GetMapping("/getWizard")
public ResponseEntity<WizardScreenDTO> getWizard(@RequestParam Long id, @RequestParam WizardSceenEnum wizardSceenEnum)
{
return wizardService.getWizard(id,wizardSceenEnum);
}

@PostMapping(value = "postWizard")
public ResponseEntity<WizardScreenDTO> postWizard(@RequestBody WizardRequestDTO wizardRequestDTO)
{
return wizardService.postWizard(wizardRequestDTO);
}

}

We have two endpoints

  • getWizard enpoint : Long id parameter and enum parameter about which screen will be managed.
  • postWizard endpoint : In the WizardRequestDTO type, it takes a wrap DTO parameter, which includes DTOs that allow other screens to be managed as well. In this wrap DTO, there is an enum value of the requestType type related to which screen will be managed and the action to be taken (CREATE, UPDATE).

All endpoints return WizardScreenDTO object. In this wrap DTO, if we want all DTOs, there are DTO objects that we need.

We can test endpoints in Postman

get endpoint :

post endpoint :

It’s been a long post. I kept the logic part a little simple to explain the process.

I hope I was helpful.

Github code: https://github.com/serdaralkancode/spring-wizard-strategy-pattern

Thanx for reading

--

--

Serdar A.

Senior Software Developer & Architect at Havelsan Github: https://github.com/serdaralkancode #Java & #Spring & #BigData & #React & #ReactNative & #JS #Microserv