Refactoring File Encryption/Decryption Logic Using the Strategy Pattern
Introduction
In a recent project, we replaced our existing file encryption solution from DRM to a new system based on MIP.
If all files had simply been processed with MIP, the task would have been straightforward. However, one of the key requirements was to maintain compatibility with legacy DRM files that had already been downloaded in the past. In other words, during file uploads, both DRM and MIP encryption/decryption mechanisms needed to coexist and operate dynamically depending on the situation.
As we tried to force this requirement into the existing structure, several limitations became apparent. This led us to refactor the system using the Strategy Pattern in an object-oriented manner, and I’d like to share that experience.
Problems with the Existing Code: Limitations of Static Utilities
The original file encryption/decryption logic was implemented as static methods within a shared utility class.
While it might have been tempting to simply add conditional branches to handle new requirements, I concluded that maintaining this approach would introduce several structural issues:
1. Risk of Shotgun Surgery and High Coupling
The encryption/decryption logic was being called from more than 12 different locations in the business logic.
If we continued with the existing approach, any change or addition to the encryption method would require modifying all 12 call sites. This is a classic case of shotgun surgery, leading to a rigid and tightly coupled system.
2. Lack of Parameter Consistency and Extensibility
DRM and MIP require different sets of parameters.
Handling them as individual parameters in a single method would mean that every time a new solution is introduced, the method signature would change, breaking all existing callers. To ensure flexibility and consistency, these parameters needed to be unified into a single DTO.
3. Difficulty in Writing Unit Tests
Static methods do not maintain state and are difficult to mock, which makes it challenging to write isolated unit tests for the business logic that depends on them.
Solution: Separating What Changes from What Does Not Using the Strategy Pattern
Business logic must evolve dynamically based on requirements, but the existing static structure made this difficult and error-prone.
To fundamentally address these issues, we redesigned the architecture by applying the core design principle of separating what changes from what does not.
To achieve this in a natural and scalable way, we introduced the Strategy Pattern and clearly divided responsibilities within the encryption/decryption system:
- What changes: Concrete encryption/decryption algorithms such as DRM and MIP, which may be replaced or extended in the future
- What does not change: The act of encrypting/decrypting files itself and the unified parameter structure used to transfer data
As a result, instead of directly depending on concrete algorithms, we abstracted them through interfaces.
This allowed the 12 client components to operate independently of specific encryption implementations.
In practice, we defined an interface with methods for encryption, decryption, and identifying the encryption type. DRM and MIP implementations inherit from this interface and provide their own logic. These implementations are registered as Spring Beans, making them easily accessible across modules.
Conclusion and Expected Benefits
Through this refactoring, all 12 client components now simply interact with a FileEncryptor abstraction without needing to know which specific algorithm is being used. This effectively achieves the Open-Closed Principle.
By removing rigid static methods, unit testing became significantly easier. Moreover, even if a third encryption solution is introduced in the future, the existing service logic will not require any modification.
This experience went beyond handling immediate branching logic—it was a valuable opportunity to deeply consider system extensibility and long-term maintainability for future developers.
pong