How I applied the Strategy Pattern to handle multiple source systems and keep the utility scalable
Introduction
While working on a Java-based utility, I encountered a challenge: moving data between different systems that each had their own export formats. Our target system was Jira Cloud, which accepted data in a specific JSON format through its APIs.
The complication came from the source systems. One example was GitHub, where I needed to export issues in JSON. Other systems exported their data in entirely different formats. The requirement was clear: I needed a flexible solution that could:
- Support multiple source systems, each with different data formats
- Transform the extracted data into a unified format compatible with Jira
- Be easily extendable to support new platforms in the future
The load step (pushing data to Jira) was straightforward. The real complexity lay in the extract and transform steps. That’s where the Strategy Pattern turned out to be the right approach.
Why the Strategy Pattern?
The Strategy Pattern allows you to encapsulate different algorithms (or logics) into separate classes and make them interchangeable at runtime.
For our use case, the “algorithms” were the extraction and transformation processes, which varied depending on the source system. Using the Strategy Pattern gave us two key benefits:
- Flexibility: I could easily switch between different extraction and transformation strategies at runtime based on the source system.
- Extensibility: Adding support for a new platform meant just implementing a new strategy without changing the existing code.
Defining the Strategy Interfaces
I broke the problem into two main steps: Extract and Transform.
Extract Strategy
public interface ExtractStrategy {
String extractData();
}
Transform Strategy
public interface TransformStrategy {
String transformData(String rawData);
}
Implementing Strategies for GitHub
Let’s take GitHub as an example.
// Extract strategy for GitHub
public class GitHubExtractStrategy implements ExtractStrategy {
@Override
public String extractData() {
// Simulating extraction from GitHub
return "{ \"title\": \"Issue 101\", \"body\": \"Sample issue description\" }";
}
}
// Transform strategy for GitHub data
public class GitHubTransformStrategy implements TransformStrategy {
@Override
public String transformData(String rawData) {
// Convert GitHub issue JSON into Jira-compatible JSON
return "{ \"fields\": { \"summary\": \"Issue 101\", \"description\": \"Sample issue description\" } }";
}
}
Similarly, if another system like ServiceNow was introduced later, I could create ServiceNowExtractStrategy
and ServiceNowTransformStrategy
without changing existing code.
Context to Tie Strategies Together
I used a Context class to apply the strategies dynamically:
public class DataProcessor {
private ExtractStrategy extractStrategy;
private TransformStrategy transformStrategy;
public DataProcessor(ExtractStrategy extractStrategy, TransformStrategy transformStrategy) {
this.extractStrategy = extractStrategy;
this.transformStrategy = transformStrategy;
}
public void process() {
String rawData = extractStrategy.extractData();
String transformedData = transformStrategy.transformData(rawData);
// Load step (pushing to Jira)
System.out.println("Loading to Jira: " + transformedData);
}
}
Using the Strategies at Runtime
The client code simply decides which strategies to use:
public class Main {
public static void main(String[] args) {
// GitHub strategies
ExtractStrategy extractStrategy = new GitHubExtractStrategy();
TransformStrategy transformStrategy = new GitHubTransformStrategy();
DataProcessor processor = new DataProcessor(extractStrategy, transformStrategy);
processor.process();
// In future, just plug in new strategies for other platforms
}
}
Outcome and Takeaways
With this design, I achieved the following:
- Each source system’s extraction and transformation logic stayed isolated.
- The utility was easy to extend and supporting a new system was just a matter of writing a new strategy.
- The load step remained independent of extraction and transformation.
The Strategy Pattern fit naturally into the Extract-Transform-Load (ETL) process, letting us build a scalable and maintainable migration utility.
Handling Different Data Formats
While strategies in our utility were tied to platforms, I noticed that data formats (JSON, CSV, XML) played an important role inside each strategy.
For example:
- GitHub APIs exported JSON.
- Some other platforms exported CSV.
- Others used XML.
Instead of making data formats their own strategies, I treated them as implementation details within platform-specific strategies. This way, the decision of which format to parse remained encapsulated in each platform strategy, keeping the overall design clean and platform-centric.
Wrap-up:
This is how I used the Strategy Pattern in a real-world scenario. It not only solved the immediate problem of handling GitHub and other systems but also gave us a foundation to support future integrations with minimal changes.
Thank you for reading! I’d love to hear your thoughts or questions in the comments. If you are passionate about building global-ready products, let’s connect on X (@gauravnadkarni) or reach out to me via email (nadkarnigaurav@gmail.com).
Top comments (0)