DEV Community

Cover image for The Anti-Corruption Layer Pattern
Ahmed Shirin
Ahmed Shirin

Posted on

The Anti-Corruption Layer Pattern

It is no secret that integrating with legacy systems is a no fun milestone. Poor documentation, lack of support, messy interfaces and more than a handful of dormant bugs are just a subset of the problems you might face in the integration process. Yet more than often, integration is an absolute necessity for technical and/or political reasons. Integration with legacy systems imposes a risk on the system under design since legacy models are usually poorly designed and thus your new, well designed model is very likely to be corrupted by the legacy integration.

Integration Strategies:

Developers who need to work with legacy systems are faced with four choices:

1- Ditch the legacy: Having a common use case isn't always a justification for integration. Developers should consider this option by scrutinizing the value of the legacy system and weighing it in against the cost of integration.

2- Conform to the legacy: When the organization has no plans to replace the legacy system, and when the quality of the legacy model is acceptable enough, then diligently conforming to the legacy model would eliminate all integration costs. A conformist approach must be seriously considered in situations where peripheral extensions are being added to a well established, dominant legacy system. This choice is highly underrated due to our irresistible infatuation with re-inventing the wheel and wasting time and money working out solutions that deliver little to no business value.

3- Anti-Corruption Layer: A highly defensive strategy to isolate your model from corruption by legacy models.

4- Replacing the Legacy: The least favorable option. Developers should refrain from phasing out large and complex models of a legacy system (specially when these models belong to the system's Core Domain) while working simultaneously on the new model, as these ambitious attempts are more susceptible to undesired outcomes.

Before attempting an Anti-Corruption Layer solution, you should carefully rethink options 1 and 2; Anti-Corruption layers can get very ugly and complicated in addition to the fact that they compose new elements to be added to the organization's context map that require maintenance and support. Before approaching the Anti-Corruption Layer pattern let's talk briefly about two prominent design patterns:

Façade:

A Façade is a pattern that is used to provide a simpler interface to a complex system. The Façade should adhere to the interfaced system's model and refrain from introducing a new model of its own (i.e: They should speak the same language).

Think of a trip planning system that has the following interface:



public interface ITripService
{
    void BookAirline();
    void ReserveHotelRoom();
    void ReserveTouristAttractionTickets();
}


Enter fullscreen mode Exit fullscreen mode

A simpler interface could be provided -when needed- through the following Façade which co-ordinates the execution of the entire operation:



public interface ITripServiceFacade
{
    void PlanItinerary();
}


Enter fullscreen mode Exit fullscreen mode

Adapter:

A Charger Adapter

A Wall Adapter

An Adapter is a pattern which allows communication between two incompatible abstractions by providing an interface that the client can make sense of. The adapter is allowed to speak the language of the client and adapt it to the other sub-system.

Think of a ride hailing application having the following interface:



public interface ITransportationService
{
    void StartTrip(string country, Zipcode from, Zipcode to);
}


Enter fullscreen mode Exit fullscreen mode

However, when insight emerged that - in some countries - trips should be fared based on the exact co-ordinates of the source and destination locations instead of zipcodes, an Adapter was provided:



public interface ITransportationServiceAdapter
{
    void StartTrip(Coordinates from, Coordinates to);
}


Enter fullscreen mode Exit fullscreen mode

Notice that the Adapter speaks the language of the client and is responsible for initiating the corresponding request to the adaptee.

Anti-Corruption Layer:

The Great Fire

The Great Fire engulfing the city of Rome

An Anti-Corruption Layer combines a set of Façades, Adapters and Translators to isolate a model from corruption by other models it needs to integrate with. The Anti-Corruption Layer might be uni-directional in case of the existence of a Customer-Supplier relationship between the two models or bi-directional otherwise.

To illustrate the pattern, let's first consider an e-Commerce application that displays an order's shipping costs by estimating the number and dimensions of the packages required to ship the order then uses a complex network traversal algorithm to find the cheapest route for shipping. From this we can quickly deduce two distinct contexts: Shipping and Packaging, each with different capabilities and a completely independent model.

Turns out that the organization already owns a legacy Packaging service that has been used for years - since the organization has previously specialized in the packaging industry before shifting the strategy towards e-Commerce. As the architects closely examined the legacy Packaging service, they realized that integration with the legacy is inevitable since the service is highly complicated (indicating that a-lot of knowledge has been crunched into the legacy) and therefore abandoning the legacy or attempting a large scale replacement are unwieldy solutions. A closer examination also ruled out a conformist approach since the legacy model is very outdated and made assumptions that do not fit anymore into the current business model, therefore conforming the Shipping context to the legacy Packaging model would cause a high rate of corruption on the Shipping side.

Unfortunately, the Packaging service is too old, undocumented, and is notorious for a number of bugs that forced several ad hoc workarounds across other components in the system, moreover, the original developers who wrote the service have departed from the organization leaving the Shipping team completely on its own. At this point, the architects decided that the Shipping context should be integrated with the Packaging context with an Anti-Corruption layer isolating the Shipping model from the legacy.

Let's take a closer look to the legacy Packaging model to highlight the model weaknesses and how they can corrupt the Shipping model:



namespace Warehouse.Packaging 
{
    public class Container 
    { 
        //Irrelevant details
    }

    public class Package 
    { 
        public double LengthInInches { get; }
        public double WidthInInches { get; }
        public double HeightInInches { get; }
    }

    public class Item 
    {
        public double LengthInInches { get; }
        public double WidthInInches { get; }
        public double HeightInInches { get; }
    }

    public class Warehouse 
    {
        public string Code { get; }
        public string ZipCode { get; }
        public IEnumerable<Container> AvailableContainers { get; }
        //Further details of a messy model
    }

    public interface ILegacyPackagingService 
    {
        bool ValidateItemDimensions (IEnumerable<Item> items);
        IEnumerable<Package> PackageItems (IEnumerable<Item> items, Warehouse warehouse);
        IEnumerable<Package> OptimizePackaging (IEnumerable<Package> packages);
    }
}


Enter fullscreen mode Exit fullscreen mode

Several problems exist in the above domain model:

1- The ILegacyPackagingService offers a complex interface where Validation, Packaging and Optimization are provided as three separate APIs - for a good reason probably - however this doesn't fit well with the Shipping model's needs since all of these operations are viewed as a single operation from the Shipping's point of view.

2- The Item and Package models use the Imperial System of measurement which presents a conflict with a new organization-wide policy to adopt the metric system in all new models.

3- The Warehouse model in the Packaging module is highly outdated and no longer reflects how the business models its own Warehouses.

4- The PackageItems API defined in the ILegacyPackagingService introduces a coupling between the Packaging logic and the Warehouse model - since packaging was previously viewed as an operation that can only take place within one of the organization's designated warehouses. However, the current Shipping model cannot sustain such an assumption since it allows products to be shipped directly from the external Vendors' storage facilities.

Obviously, the Anti-Corruption Layer has a-lot of work to do. Allowing any of the above mentioned problems to leak into the Shipping model would yield a corrupted model that carries all the weaknesses of the legacy system. To mitigate the first issue, we introduce a Façade within the Anti-Corruption Layer:



namespace Warehouse.Packaging.AntiCorruption.Shipping 
{
    public interface IPackagingServiceFacade 
    {
        IEnumerable<Warehouse.Packaging.Package> PackageItems (IEnumerable<Item> items, Warehouse warehouse);
    }

    public class PackagingServiceFacade : IPackagingServiceFacade 
    {
        private readonly ILegacyPackagingService _packagingService;

        public PackagingServiceFacade (ILegacyPackagingService packagingService) 
        {
            _packagingService = packagingService;
        }

        public IEnumerable<Warehouse.Packaging.Package> PackageItems (IEnumerable<Item> items, Warehouse warehouse) 
        {
            if (_packagingService.ValidateItemDimensions (items)) 
            {
                var packages = _packagingService.PackageItems (items, warehouse);
                var optimizedPackages = _packagingService.OptimizePackaging (packages);
                return optimizedPackages;
            }

            return Enumerable.Empty<Package> ();
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Note that the Façade uses the same model as the Packaging service, no model elements were added and the language is maintained. The Façade only offers a friendlier interface to the model. The Façade is even placed under the Warehouse.Packaging module/namespace which encompasses the Packaging context (Anti-Corruption layers can span multiple modules).

To account for the second problem, the Anti-Corruption layer will define a translator object to map between the different models used in the two contexts, in the following example we are using AutoMapper to illustrate the mapping operation in a simple manner:



namespace Logistics.Shipping.AntiCorruption.Packaging
{
    public class PackagerProfile : Profile
    {
        private const double InchesCmConversionConst = 0.39370;

        public PackagerProfile()
        {
            CreateMap<Product, Item>()
                .ForMember(dst => dst.LengthInInches, opt => opt.MapFrom(src => src.LengthInCm * InchesCmConversionConst))
                .ForMember(dst => dst.WidthInInches, opt => opt.MapFrom(src => src.WidthInCm * InchesCmConversionConst))
                .ForMember(dst => dst.HeightInInches, opt => opt.MapFrom(src => src.HeightInCm * InchesCmConversionConst));

            CreateMap<Warehouse.Packaging.Package, Logistics.Shipping.Package>()
                .ForMember(dst => dst.LengthInCm, opt => opt.MapFrom(src => src.LengthInInches / InchesCmConversionConst))
                .ForMember(dst => dst.WidthInCm, opt => opt.MapFrom(src => src.WidthInInches / InchesCmConversionConst))
                .ForMember(dst => dst.HeightInCm, opt => opt.MapFrom(src => src.HeightInInches / InchesCmConversionConst));
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

The translator can be used back and forth to map the incompatible models.

To handle the third and forth issues, we introduce an Adapter to the Anti-Corruption layer which provides the Shipping service with a more compatible interface that is loosely coupled from the Warehouse model and instead uses the Container value object to represent the packaging capabilities:



namespace Logistics.Shipping.AntiCorruption.Packaging
{
    public interface IPackagingService 
    {
        IEnumerable<Logistics.Shipping.Package> PackageItems (IEnumerable<Product> items, IEnumerable<Container> containers);
    }

    public class PackagingServiceAdapter : IPackagingService 
    {
        private readonly IPackagingServiceFacade _packagingServiceFacade;
        private readonly IMapper _mapper;

        public PackagingServiceAdapter (IPackagingServiceFacade packagingServiceFacade, IMapper mapper) 
        {
            _packagingServiceFacade = packagingServiceFacade;
            _mapper = mapper;
        }

        public IEnumerable<Logistics.Shipping.Package> PackageItems (IEnumerable<Product> products, IEnumerable<Container> containers) 
        {
            Warehouse warehouse; //Logic to initialize a transient dummy warehouse
            var items = _mapper.Map<IEnumerable<Item>> (products); //Using the translator to map the Product model to an Item
            var packages = _packagingServiceFacade.PackageItems (items, warehouse); //Calling the Façade 
            return _mapper.Map<IEnumerable<Logistics.Shipping.Package>> (packages); //Mapping the Packager's result to the Package model defined in the Shipping context
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

The outcome is a Shipping model that is integrated with the legacy Packaging service without being corrupted by the legacy:



namespace Logistics.Shipping 
{
    public class Zone 
    { 
    }

    public class Product 
    {
        public double LengthInCm { get; }
        public double WidthInCm { get; }
        public double HeightInCm { get; }
    }

    public class Package 
    {
        public double LengthInCm { get; }
        public double WidthInCm { get; }
        public double HeightInCm { get; }
    }

    public class Courier 
    {
        private IPackagingService _packagingService;

        public double GetQuote (IEnumerable<Product> items, Zone source, Zone destination) 
        {
            var packagingCapabilities = _capabilitiesService.GetPackagingCapabilities (source);
            var packages = _packagingService.PackageItems (items, packagingCapabilities.Containers);
            //Shipping specific logic goes here
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

A rough map of the domain model would be as follows:

Domain Model

Details concerning the Anti-Corruption layer are omitted for simplicity.

Conclusion

Great Wall of China

The Great Wall of China

Developing an Anti-Corruption layer is a time consuming process that requires lots of analysis and development work. Architects should not jump to this solution at once and instead consider other simple alternatives, however when there is no other choices, an Anti-Corruption layer - just like a wall surrounding a fortress from hostile invasion - will protect your model from outer influences and allow you to work in isolation.

Top comments (2)

Collapse
 
thorstenhirsch profile image
Thorsten Hirsch • Edited

Well, there's another option. There's actually a whole IT sector focusing on application integration. If you're lucky your company has an EAI system (enterprise application integration), sometimes called ESB (enterprise service bus) that will do the "no fun" things for you. I am working as an EAI developer myself and we have the following goals:

  • replacing an integrated application shall have no effect on other applications (only the EAI interfaces must be adapted to the new application)
  • versatility: the EAI platform supports all required protocols and integration patterns
  • monitoring: all data flows must be tracked (so that the data owners know who else received their data)

The anti-corruption layer in Logistics violates our 1st rule, because it implements the Warehouse interface. Here's what an EAI solution would look like:

  • implement the Warehouse interface
  • implement the Logistics interface
  • implement a mapping between Logistics and Warehouse interface

Sounds pretty similar to the anti-corruption layer? Yes, but the result is a passive component running on the EAI platform that can even support different protocols (think of "message-oriented-middleware"), so Logistics might call the EAI via REST and the Warehouse might want to receive a CSV file. But most important is that neither application depends on the other, because none of them has implemented the interface of the other.

An EAI platform might not be worth it when you only have a handful applications talking to each other and compare the development effort (and the additional costs for the EAI). But the aspect of making the data flows between your applications transparent is really of great value and makes an EAI platform attractive even if you pay a bit more in sum.

Collapse
 
rafalpienkowski profile image
Rafal Pienkowski

@thorstenhirsch, I agree with our comment. In the case where different modules communicate each other (no matter how: SOAP, REST API, flat files, etc.) or in the enterprise world (where the budget is high enough to buy and maintain such kind of ESB system) using EAI platform brings a lot of benefits (as you mentioned).

On the other hand, when I'm working in a bigger team, and there is no need for usage out of process communication, I think that the Anti-Corruption Layer (ACL) is more convenient. Teams can develop their libraries/packages independently without affecting models in my part of the system, as it was described in this article.

To sum up, depending on the project size and communication type (in/out of process) EAI platform and ACL can be used interchangeably and/or in conjunction.