While Creating Something New, Take Care of Those Who Use the Old
How I Implemented an Anti-Corruption Layer
Gene Zeiniss
Designed by Freepic.diller / Freepik
Every family’s got its own chronicle. For the most part, this private past is “caught on tape”. I’m referring to photography. Think about it, were it not for photographs, you’d never recognize what your first birthday cake looked like.
The memory of that cake would have faded away if not for the constant reminder. How else would your parents fondly argue about whom you looked like more?
I remember my Mom experimented (on me) with her new camera 📷: “Look at me and smell the flowers!”, “Smile!”. Then she did her magic in the “red room” which resulted in black-and-white photos in different sizes.
The good photos she added to the family album, others were kept in a big “Chumodan”. These compromising pieces of evidence of my childhood still exist. Mom (probably like all moms in the world) doesn’t hesitate to put them up for public inspection.
Photography has come a long way in its fairly short history. In almost 200 years, the camera evolved from a simple box that took blurred images of sneezing women to the high-tech micro computers used in today’s DSLRs and smartphones.
As cameras began to use a digital medium instead of film, the storage of produced images was changed as well from physical albums to electronic storage.
With all our love of progress, we must remember that some people, like my Mom, are too tied to their photo albums. “Cloud is good, but a physical album is better”, she says, “But I can’t feel it!”, she complains. So, the challenge is to implement the new technology but to enable Moms everywhere to keep their albums.
I mean, we must remember that some users take more time to become accustomed to new technologies. In a hi-tech world, the Anti-Corruption Layer allows a gradual transition from the Old to the New.
At Behalf, the company I work for, the legacy system was written as a monolith and used a non-transactional database. My example will use the word “Film-Camera” to represent the Monolith and word “Photo-Album” to call the legacy database.
The plan was to rewrite the system on microservices, using Domain-Driven Design architecture and to replace a non-transactional database by a transactional one. My example will use “Digital Camera” as the Microservice domain and it is connected to a database we’ll call “Electronic-Storage”
Our research showed that while we build the new “Digital-Camera” service we will still have users (Mooooooom!) that will stick to the old technologies. This meant that while the world moved on, some legacy must be maintained while users transition.

As we continued we realized the two services were completely incompatible. The old world “Film-Camera” and cutting edge “Digital-Camera” had a different semantic and different life cycles of their entities, namely, photographs. So, the rollout plan for Digital-Camera service had to include the implementation of an Anti-Corruption Layer.
Anti-Corruption Layer: The Theory
Anti-Corruption Layer is a fancy term for Glue Code, that integrates the Microservice into the Monolith, so they can share data. This term first appeared in the book “Domain-Driven Design” by Eric Evans. This glue code receives the name “Anti-Corruption Layer” because it allows keeping your brand-new and shiny service’s code as being pristine.
Think about it, digital images don’t decay or expire, so there’s no need to track ink aging or degradation that happens to physical images. The limitation of the number of images on an album page is not a concern in digital storage. Albums must be cataloged or the Chumodan explodes eventually — that is not a concern for the storage of the digital images.
On the other hand, albums are assumed to be public. They are exposed to all because of moms 🤦♀️. Digital images are not assumed to be public at all — so require authorization logic that would not exist in the Monolith. These are just a few examples of legacy concepts, our Microservice should not handle or even know about.
The Anti-Corruption Layer prevents legacy concepts from polluting and corrupting the domain model of your new service, by adding some intermediate layer.
Broadly speaking, an Anti-Corruption Layer looks like this:

On the left, we got the Microservice, on the right we got the Monolith. The Anti-Corruption Layer is placed between the two. This layer has an API that uses the domain language of the Microservice. At the other end of the Anti-Corruption Layer is a Facade that effectively wraps the features of monolithic systems and hides the legacy language.
Between the two is an Adapter that implements the API and translates between two separate domain models using a Translator interface.
Anti-Corruption Layer: In Practice
We want to reflect images from the Digital-Camera domain to the Film-Camera monolith (and its domains). Here’s how we do it:

As you can see in the diagram above, my Anti-Corruption Layer is part of both services. The Translator is on the Microservice side and the Adapter is on the Monolith side. This structure encourages a static legacy monolith. After we’re done with this we’d like to stop touching the Monolith.
Instead, we want to focus on the new domain’s service and capabilities. Changes to the microservices require changes only to the translator — which is part of the microservice and therefore intimate with those changes.
In this example I’m missing the Facade. My example is contrived so it does not require a Facade to make the monolithic API adhere to workflows or be simpler to use. So I dropped it for brevity. You may assume that we have a Facade identical to the repository it wraps and provides the same API.
Now let’s dive to the nitty-gritty.
Digital-Camera Service
Our objective is to represent the Image generated by Digital-Camera on legacy Photo-Albom. We can safely assume that image creation is initiated by some API the service exposes. Such an API would normally call a service to create a new image using the Image Repository layer.
Usually, this is where our story would end (with a new table record) but this is actually just the edge of our Anti-Corruption Layer. A brief context interlude:
At Behalf, we use a Domain-Driven Design architecture, where each business domain has a dedicated service and a dedicated database. The services communicate with each other asynchronously, by producing Business Events. Each produced event tells the world that something happened to a domain’s entity.
The event’s payload includes only a specific entity’s identification. Other concerned domains have the ability to retrieve the data owned by the original domain. They consume the events and carry out their own actions.
This is why it was decided to implement communication inside the Anti-Corruption Layer by events as well. However, I really wanted to prevent Film-Camera from retrieving data from my Digital-Camera database directly. So I created a new kind of events — Record Events. These events notify that a specific record (image record, in our case) was created in the Microservice database (Electronic-Storage).
The event is triggered by the Repository call that makes the changes and its payload includes relevant record details (not only the identification). Actually, it includes the all data Film-Camera should know to create its own record of the new Photography and to place it in its own database (Photo-Album).
But, one at a time, back to Image Repository. My Image Repository extends some BaseRepository (from the core layer) and overrides the base’s postCreate()
method. On image record creation, it’s called Image Record Event Producer class. In other words, it triggers the reflection process.
@Repository
@AllArgsConstructor
public class ImageRepository extends BaseRepository<Image, ImageRecord> {
private final ImageRecordEventProducer imageRecordEventProducer;
@Override
protected void postCreate(Image image) {
imageRecordEventProducer.sendImageRecordCreatedEvent(image);
super.postCreate(image);
}
}
Image Record Event Producer and its Dependency:
The Image Record Event Producer generates Image Record related events. In our example it generates only a Created event. Events carry a payload and in our case this payload’s semantics are similar to the Film-Camera domain language. The event should “speak photography”, so to speak. However, since the two domains do have some differing semantics we cannot just send over the raw image, it must be converted.
Let us assume the digital image has a resolution parameter. The higher the resolution of an image, the better the quality of the image you have. Image quality affects the maximum available frame of photography. What other parameters can we throw in here?
If you were born a Gen-Xer or at the beginning of the Gen-Y, you probably remember the default timestamp that burned onto photographs by the simple box-cameras. It was in a specific format: one or two digits for day, long space (maybe double), then one or two digits for month, apostrophe, two digits for the year. 30 April 2002 looked like 30 4’02.
To summarise, our image class has the following parameters:
@Data
public class Image {
private String id;
private Resolution resolution;
private Instant createdOn;
private Instant modifiedOn;
}
Image Record Created event’s parameters are pretty similar to Photography parameters:
@Value
@Builder
public class ImageRecordCreatedEvent {
String imageRefId;
String dateOnPhoto;
String maxAvailableFrame;
}
The event includes the maximum available frame parameter, which is derived from the minimum image resolution. The event has an image reference id parameter. We will need this parameter in order to associate between records in separate domains. It will assist us in testing, monitoring, and record updating processes.
The conversion of Image to the event payload includes some logic. To keep the event producer clean of logic, I created the Image Translator class, which encapsulates the dirty job of conversion.
@Service
public class ImageTranslator {
protected ImageRecordCreatedEvent translateImageToRecordCreatedEvent(Image image) {
return ImageRecordCreatedEvent.builder()
.imageRefId(image.getId())
.maxAvailableFrame(translateImageResolutionToMaxFrame(image.getResolution()))
.dateOnPhoto(translateImageCreationDateToDateOnPhoto(image.getCreatedOn()))
.build();
}
private String translateImageResolutionToMaxFrame(Resolution resolution) {
return API.Match(resolution).of(
Case($(SIZE_4680_X_6620), () -> "A1"),
Case($(SIZE_3300_X_4680), () -> "A2"),
Case($(SIZE_2340_X_3300), () -> "A3"),
Case($(SIZE_2490_X_3510), () -> "A4"),
Case($(SIZE_1740_X_2490), () -> "A5"),
Case($(SIZE_1230_X_1740), () -> "A6"),
Case($(SIZE_870_X_1230), () -> "A7"),
Case($(SIZE_600_X_870), () -> "A8"),
Case($(SIZE_450_X_600), () -> "A9"),
Case($(SIZE_300_X_450), () -> "A10"));
}
private String translateImageCreationDateToDateOnPhoto(Instant imageCreatedOn) {
LocalDate imageCreationDate = imageCreatedOn.atZone(ZoneOffset.UTC).toLocalDate();
int date = imageCreationDate.getDayOfMonth();
int month = imageCreationDate.getMonthValue();
int year = imageCreationDate.getYear();
return String.format("%s %s'%s", date, month, String.valueOf(year).substring(2));
}
}
Here is how my Image Record Event Producer looks. It depends on Image Translator, which translates the image to the event, and Event Producer, that actually generated the events.
@Component
@RequiredArgsConstructor
public class ImageRecordEventProducer {
private final ImageTranslator imageTranslator;
private final EventProducer eventProducer;
public void sendImageRecordCreatedEvent(Image image) {
ImageRecordCreatedEvent event = imageTranslator.translateImageToRecordCreatedEvent(image);
eventProducer.sendEvent(event);
}
}
The Digital-Camera part of the Anti-Corruption Layer is implemented. Check! Now let’s take a look at the Film-Camera domain side of our layer.
Film-Camera Service
Film-Camera consumes the Record Events, instantiates Photography objects from received data, adapts the information if needed, and finally creates Photography records in the Photo-Album database.
Event consumption happens in the Reflect Image On Photography Actor class. An Actor class receives the event and performs a specific action, implied by the Actor’s name. In our case, it’s a kind of passthrough interface, because all it does is to redirect to the relevant method in the Photography Adapter class. YMMV.
@Service
@RequiredArgsConstructor
public class ReflectImageOnPhotographyActor {
private final PhotographyAdapter photographyAdapter;
@EventListener
public void onImageRecordCreatedEvent(ImageRecordCreatedEvent event) {
photographyAdapter.reflectImageCreationToPhotography(event);
}
}
Photography Adapter:
The Adapter represents the digital image as physical photography.
Let us assume you made a perfect picture on your phone. Now you want to share it with “old-school” someone (if it’s my Mom, she will want this picture in her album). Your first step will be to select the picture size that will fit the album (and match the resolution for a nice crisp image).
Next select the paper type, then print the picture, and add it to the album (and of course add a cute caption). In other words, you will adapt the digital image to the physical photography domain. That’s exactly what my Photography Adapter class does.
I map the Image Record Created event data to Photography. As I mentioned previously, the event’s payload is very close to Photography parameters.
@Data
public class Photography {
private String imageRefId;
private String dateOnPhoto;
private Frame maxAvailableFrame;
private Size optimalSize;
...
}
I use the Model Mapper for easy conversion:
@Service
@RequiredArgsConstructor
public class PhotographyMapper {
private final ModelMapper modelMapper;
protected Photography mapImageRecordToPhotography(ImageRecordCreatedEvent event) {
return modelMapper.map(event, Photography.class);
}
}
Next I enrich the initiated Photography (by applying the adapter logic) with parameters such as optimal size, paper type, and all those other Photography domain terms. Finally, I add the photograph to the Photo-Album by calling the Photography Repository’s create()
method 😅.
Take a look at how my adapter looks like:
@Slf4j
@Service
@RequiredArgsConstructor
public class PhotographyAdapter {
private final PhotographyMapper photographyMapper;
private final PhotographyRepository photographyRepository;
protected void reflectImageCreationToPhotography(ImageRecordCreatedEvent event) {
Photography photography = photographyMapper.mapImageRecordToPhotography(event);
photography.setOptimalSize(calculateOptimalSizeAccordingToMaxFrame(photography.getMaxAvailableFrame()));
log.info("select the paper type");
log.info("print the photography");
log.info("photography is about to be added to photo-album");
photographyRepository.create(photography);
}
private Size calculateOptimalSizeAccordingToMaxFrame(Frame maxAvailableFrame) {
return API.Match(maxAvailableFrame).of(
Case(API.$(Frame.A1), Size.R10),
Case(API.$(Frame.A2), Size.R10),
Case(API.$(Frame.A3), Size.R8S),
Case(API.$(Frame.A4), Size.R8),
Case(API.$(Frame.A5), Size.R6),
Case(API.$(Frame.A6), Size.R5),
Case(API.$(Frame.A7), Size.R4),
Case(API.$(Frame.A8), Size.R3),
Case(API.$(Frame.A9), Size.R2),
Case(API.$(Frame.A10), Size.R1));
}
}
That’s all! You have a nice image, your Mom has it too in her photo-album. Everyone’s happy!
In my example, the data is shared in a one-way direction (from Microservice to Monolith). my Digital-Camera service is totally independent. However, my Mom would easily implement two-way communication. She scans photographs and shares them in social networks.
She particularly favors uploading the photos of mine (the same photos that were stuck in the photo albums) and she is still a little upset when I refuse to add tagged images to my timeline. However, the “parents in a social media” topic deserves it’s own article.
Maybe next time.
This article was originally published on medium.
Upvote
Gene Zeiniss
Java developer, backend guild master at a fintech startup, blogger (http://medium.com/@genezeiniss) 🤓

Related Articles