Implementing the Strategy Design pattern in Spring Boot

Goutham
CodeX
Published in
5 min read1 day ago

The Strategy Design Pattern is a behavioral pattern that enables us to select an algorithm’s behavior at runtime. This pattern lets us define a set of algorithms, place them in different classes, and makes them interchangeable [1].

This is just a definition but let’s get a better understanding by knowing the problem that we are trying to solve.

The Problem

Let’s say you are working on a feature called File Parser. You need to write an API where you can upload a file and our system should be able to extract the data from it and persist them in the database. Currently we are asked to support CSV, JSON and XML files. Our immediate solution would look something like below.

@Service
public class FileParserService {

public void parse(File file, String fileType) {
if (Objects.equals(fileType, "CSV")) {
// TODO : a huge implementation to parse CSV file and persist data in db
} else if (Objects.equals(fileType, "JSON")) {
// TODO : a huge implementation to parse JSON file and persist data in db
} else if (Objects.equals(fileType, "XML")) {
// TODO : a huge implementation to parse XML file and persist data in db
} else {
throw new IllegalArgumentException("Unsupported file type");
}
}

}

Everything looks good now from the business perspective but things will start getting uglier when we want to support more file types in the future. We start adding multiple else if blocks and the size of the class will quickly grow which will eventually become too hard to maintain. Any change to one of the implementations of the file parser will affect the whole class thereby increasing the chance of introducing a bug in an already working functionality.

Not only that, but there is another problem. Let’s say now we need to additionally support sqlite and parquet file types. Two developers will step in and they will start working on the same huge class. It is highly likely that they will get merge conflicts which is not only irritating for any developer but also time consuming to resolve them. Most importantly, even after the conflict resolution, there would be decreased confidence in terms of the feature working as a whole.

The Solution

This is where the Strategy Design pattern steps in to our rescue. We will move all the file parser implementations to separate classes called strategies. In the current class, we shall dynamically fetch the appropriate implementation based on file type and execute the strategy.

Here’s a UML diagram to provide a high-level overview of the design pattern that we are about to implement.

Now, let’s just dive into the code.

We will need a class to maintain different file types supported. Later we will use this to create spring beans (i.e. strategies) with custom names.

public class FileType {
public static final String CSV = "CSV";
public static final String XML = "XML";
public static final String JSON = "JSON";
}

Create an interface for our File Parser

public interface FileParser {
void parse(File file);
}

Now that we have created an interface, let’s create different implementations for different file types i.e. strategies

@Service(FileType.CSV)
public class CsvFileParser implements FileParser {

@Override
public void parse(File file) {
// TODO : impl to parse csv file
}

}
@Service(FileType.JSON)
public class JsonFileParser implements FileParser {

@Override
public void parse(File file) {
// TODO : impl to parse json file
}

}
@Service(FileType.XML)
public class XmlFileParser implements FileParser {

@Override
public void parse(File file) {
// TODO : impl to parse xml file
}

}

Notice that we have given custom names for the above beans which will help us inject all these three beans to our required class.

Now we need to find a way to choose one of the above implementations based on file type during runtime.

Let’s create a FileParserFactory class. This class is responsible in deciding which implementation to choose given a file type. We will leverage spring boot’s awesome dependency injection feature to fetch the appropriate strategy during runtime. (Refer the comments in the below code block for more details or [2])

@Component
@RequiredArgsConstructor
public class FileParserFactory {

/**
* Spring boot's dependency injection feature will construct this map for us
* and include all implementations available in the map with the key as the bean name
* Logically, the map will look something like below
* {
* "CSV": CsvFileParser,
* "XML": XmlFileParser,
* "JSON": JsonFileParser
* }
*/
private final Map<String, FileParser> fileParsers;

/**
* Return's the appropriate FileParser impl given a file type
* @param fileType one of the file types mentioned in class FileType
* @return FileParser
*/
public FileParser get(String fileType) {
FileParser fileParser = fileParsers.get(fileType);
if (Objects.isNull(fileParser)) {
throw new IllegalArgumentException("Unsupported file type");
}
return fileParser;
}

}

Now, let’s make changes to our FileParserService. We will use our FileParserFactory to fetch the appropriate FileParser based on the fileType and call the parse method.

@Service
@RequiredArgsConstructor
public class FileParserService {

private final FileParserFactory fileParserFactory;

public void parse(File file, String fileType) {
FileParser fileParser = fileParserFactory.get(fileType);
fileParser.parse(file);
}

}

That’s it. We are done!

Conclusion

If we have to support more file types, we just have to create new classes like SqliteFileParser and ParquetFileParser which implements the FileParser interface. As a result, multiple developers implementing these new file parsers will avoid any merge conflicts later.

The existing file parsers remain untouched thereby reducing any chances of breaking existing functionality.

Additionally our code now aligns with SOLID principles particularly our beloved Open/Closed principle. By encapsulating the file parsing implementations into separate classes, we can extend the system with new parsing strategies without modifying the existing code. This makes our system more adaptable to future requirements and easier to maintain.

--

--