feat: anti corruption layer - Issue 1325. (#2777)

* add init impl

* add docs

* add test

* correct tests and specs

* add check style corrections

* add init impl

* add docs

* add test

* correct tests and specs

* add check style corrections

* issue_1325: revert accidental damage to the other repos

* issue_1325: sort the comments

---------

Co-authored-by: Boris Zhguchev <boris.zhguchev@gropyus.com>
This commit is contained in:
Boris Zhguchev
2024-03-17 08:32:24 +01:00
committed by GitHub
parent 7034ca5e95
commit 5c47447d9b
19 changed files with 677 additions and 0 deletions
+160
View File
@@ -0,0 +1,160 @@
---
title: Anti-corruption layer
category: Architectural
language: en
tag:
- Cloud distributed
- Decoupling
- Microservices
---
## Intent
Implement a façade or adapter layer between different subsystems that don't share the same semantics.
This layer translates requests that one subsystem makes to the other subsystem.
Use this pattern to ensure that an application's design is not limited by dependencies on outside subsystems.
This pattern was first described by Eric Evans in Domain-Driven Design.
## Explanation
### Context and problem
Most applications rely on other systems for some data or functionality.
For example, when a legacy application is migrated to a modern system,
it may still need existing legacy resources.
New features must be able to call the legacy system.
This is especially true of gradual migrations,
where different features of a larger application are moved to a modern system over time.
Often these legacy systems suffer from quality issues such as convoluted data schemas or obsolete APIs.
The features and technologies used in legacy systems can vary widely from more modern systems.
To interoperate with the legacy system, the new application may need to support outdated infrastructure, protocols, data models, APIs,
or other features that you wouldn't otherwise put into a modern application.
Maintaining access between new and legacy systems can force the new system to adhere to at least some of the legacy system's APIs or other semantics.
When these legacy features have quality issues, supporting them "corrupts" what might otherwise be a cleanly designed modern application.
Similar issues can arise with any external system that your development team doesn't control, not just legacy systems.
### Solution
Isolate the different subsystems by placing an anti-corruption layer between them.
This layer translates communications between the two systems, allowing one system to remain unchanged
while the other can avoid compromising its design and technological approach.
### Programmatic example
#### Introduction
The example shows why the anti-corruption layer is needed.
Here are 2 shop-ordering systems: `Legacy` and `Modern`. \
(
*It is important to state the separation is very conditional, and is drawn for learning purposes*.
*In reality the pattern does not depend on the so-called ageness but rather relies on the different domain models.*)
The aforementioned systems have different domain models and have to operate simultaneously.
Since they work independently the orders can come either from the `Legacy` or `Modern` system.
Therefore, the system that receives the legacyOrder needs to check if the legacyOrder is valid and not present in the other system.
Then it can place the legacyOrder in its own system.
But for that, the system needs to know the domain model of the other system and to avoid that,
the anti-corruption layer(ACL) is introduced.
The ACL is a layer that translates the domain model of the `Legacy` system to the domain model of the `Modern` system and vice versa.
Also, it hides all other operations with the other system, uncoupling the systems.
#### Domain model of the `Legacy` system
```java
public class LegacyOrder {
private String id;
private String customer;
private String item;
private String qty;
private String price;
}
```
#### Domain model of the `Modern` system
```java
public class ModernOrder {
private String id;
private Customer customer;
private Shipment shipment;
private String extra;
}
public class Customer {
private String address;
}
public class Shipment {
private String item;
private String qty;
private String price;
}
```
#### Anti-corruption layer
```java
public class AntiCorruptionLayer {
@Autowired
private ModernShop modernShop;
@Autowired
private LegacyShop legacyShop;
public Optional<LegacyOrder> findOrderInModernSystem(String id) {
return modernShop.findOrder(id).map(o -> /* map to legacyOrder*/);
}
public Optional<ModernOrder> findOrderInLegacySystem(String id) {
return legacyShop.findOrder(id).map(o -> /* map to modernOrder*/);
}
}
```
#### The connection
Wherever the `Legacy` or `Modern` system needs to communicate with the counterpart
the ACL needs to be used to avoid corrupting the current domain model.
The example below shows how the `Legacy` system places an order with a validation from the `Modern` system.
```java
public class LegacyShop {
@Autowired
private AntiCorruptionLayer acl;
public void placeOrder(LegacyOrder legacyOrder) throws ShopException {
String id = legacyOrder.getId();
Optional<LegacyOrder> orderInModernSystem = acl.findOrderInModernSystem(id);
if (orderInModernSystem.isPresent()) {
// order is already in the modern system
} else {
// place order in the current system
}
}
}
```
### Issues and considerations
- The anti-corruption layer may add latency to calls made between the two systems.
- The anti-corruption layer adds an additional service that must be managed and maintained.
- Consider how your anti-corruption layer will scale.
- Consider whether you need more than one anti-corruption layer. You may want to decompose functionality into multiple services using different technologies or languages, or there may be other reasons to partition the anti-corruption layer.
- Consider how the anti-corruption layer will be managed in relation with your other applications or services. How will it be integrated into your monitoring, release, and configuration processes?
- Make sure transaction and data consistency are maintained and can be monitored.
- Consider whether the anti-corruption layer needs to handle all communication between different subsystems, or just a subset of features.
- If the anti-corruption layer is part of an application migration strategy, consider whether it will be permanent, or will be retired after all legacy functionality has been migrated.
- This pattern is illustrated with distinct subsystems above, but can apply to other service architectures as well, such as when integrating legacy code together in a monolithic architecture.
## Applicability
Use this pattern when:
- A migration is planned to happen over multiple stages, but integration between new and legacy systems needs to be maintained.
- Two or more subsystems have different semantics, but still need to communicate.
This pattern may not be suitable if there are no significant semantic differences between new and legacy systems.
## Tutorials
* [Microsoft - Anti-Corruption Layer](https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer)
* [Amazon - Anti-Corruption Layer](https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/acl.html)
## Credits
* [Domain-Driven Design. Eric Evans](https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215)
@@ -0,0 +1,25 @@
@startuml
package com.iluwatar.corruption {
class LegacyShop {
private Store store;
private AntiCorruptionLayer acl;
}
class ModernShop {
private Store store;
private AntiCorruptionLayer acl;
}
class AntiCorruptionLayer{
private LegacyShop legacyShop;
private ModernShop modernShop;
}
LegacyShop ---> "findOrderInModernSystem" AntiCorruptionLayer
ModernShop ---> "findOrderInLegacySystem" AntiCorruptionLayer
AntiCorruptionLayer ..|> ModernShop
AntiCorruptionLayer ..|> LegacyShop
}
@enduml
+72
View File
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
The MIT License
Copyright © 2014-2022 Ilkka Seppälä
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>java-design-patterns</artifactId>
<groupId>com.iluwatar</groupId>
<version>1.26.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>anti-corruption-layer</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<configuration>
<archive>
<manifest>
<mainClass>com.iluwatar.corruption.App</mainClass>
</manifest>
</archive>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,42 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2023 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.corruption;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* This layer translates communications between the two systems,
* allowing one system to remain unchanged while the other can avoid compromising
* its design and technological approach.
*/
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
@@ -0,0 +1,28 @@
/**
* Context and problem
* Most applications rely on other systems for some data or functionality.
* For example, when a legacy application is migrated to a modern system,
* it may still need existing legacy resources. New features must be able to call the legacy system.
* This is especially true of gradual migrations,
* where different features of a larger application are moved to a modern system over time.
*
* <p>Often these legacy systems suffer from quality issues such as convoluted data schemas
* or obsolete APIs.
* The features and technologies used in legacy systems can vary widely from more modern systems.
* To interoperate with the legacy system,
* the new application may need to support outdated infrastructure, protocols, data models, APIs,
* or other features that you wouldn't otherwise put into a modern application.
*
* <p>Maintaining access between new and legacy systems can force the new system to adhere to
* at least some of the legacy system's APIs or other semantics.
* When these legacy features have quality issues, supporting them "corrupts" what might
* otherwise be a cleanly designed modern application.
* Similar issues can arise with any external system that your development team doesn't control,
* not just legacy systems.
*
* <p>Solution Isolate the different subsystems by placing an anti-corruption layer between them.
* This layer translates communications between the two systems,
* allowing one system to remain unchanged while the other can avoid compromising
* its design and technological approach.
*/
package com.iluwatar.corruption;
@@ -0,0 +1,44 @@
package com.iluwatar.corruption.system;
import com.iluwatar.corruption.system.legacy.LegacyShop;
import com.iluwatar.corruption.system.modern.Customer;
import com.iluwatar.corruption.system.modern.ModernOrder;
import com.iluwatar.corruption.system.modern.Shipment;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* The class represents an anti-corruption layer.
* The main purpose of the class is to provide a layer between the modern and legacy systems.
* The class is responsible for converting the data from one system to another
* decoupling the systems to each other
*
* <p>It allows using one system a domain model of the other system
* without changing the domain model of the system.
*/
@Service
public class AntiCorruptionLayer {
@Autowired
private LegacyShop legacyShop;
/**
* The method converts the order from the legacy system to the modern system.
* @param id the id of the order
* @return the order in the modern system
*/
public Optional<ModernOrder> findOrderInLegacySystem(String id) {
return legacyShop.findOrder(id).map(o ->
new ModernOrder(
o.getId(),
new Customer(o.getCustomer()),
new Shipment(o.getItem(), o.getQty(), o.getPrice()),
""
)
);
}
}
@@ -0,0 +1,25 @@
package com.iluwatar.corruption.system;
import java.util.HashMap;
import java.util.Optional;
/**
* The class represents a data store for the modern system.
* @param <V> the type of the value stored in the data store
*/
public abstract class DataStore<V> {
private final HashMap<String, V> inner;
public DataStore() {
inner = new HashMap<>();
}
public Optional<V> get(String key) {
return Optional.ofNullable(inner.get(key));
}
public Optional<V> put(String key, V value) {
return Optional.ofNullable(inner.put(key, value));
}
}
@@ -0,0 +1,25 @@
package com.iluwatar.corruption.system;
/**
* The class represents an general exception for the shop.
*/
public class ShopException extends Exception {
public ShopException(String message) {
super(message);
}
/**
* Throws an exception that the order is already placed but has an incorrect data.
*
* @param lhs the incoming order
* @param rhs the existing order
* @return the exception
* @throws ShopException the exception
*/
public static ShopException throwIncorrectData(String lhs, String rhs) throws ShopException {
throw new ShopException("The order is already placed but has an incorrect data:\n"
+ "Incoming order: " + lhs + "\n"
+ "Existing order: " + rhs);
}
}
@@ -0,0 +1,19 @@
package com.iluwatar.corruption.system.legacy;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* The class represents an order in the legacy system.
* The class is used by the legacy system to store the data.
*/
@Data
@AllArgsConstructor
public class LegacyOrder {
private String id;
private String customer;
private String item;
private int qty;
private int price;
}
@@ -0,0 +1,28 @@
package com.iluwatar.corruption.system.legacy;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* The class represents a legacy shop system. The main purpose is to place the order from the
* customers.
*/
@Service
public class LegacyShop {
@Autowired private LegacyStore store;
/**
* Places the order in the legacy system. If the order is already present in the modern system,
* then the order is placed only if the data is the same. If the order is not present in the
* modern system, then the order is placed in the legacy system.
*/
public void placeOrder(LegacyOrder legacyOrder) {
store.put(legacyOrder.getId(), legacyOrder);
}
/** Finds the order in the legacy system. */
public Optional<LegacyOrder> findOrder(String orderId) {
return store.get(orderId);
}
}
@@ -0,0 +1,13 @@
package com.iluwatar.corruption.system.legacy;
import com.iluwatar.corruption.system.DataStore;
import org.springframework.stereotype.Service;
/**
* The class represents a data store for the legacy system.
* The class is used by the legacy system to store the data.
*/
@Service
public class LegacyStore extends DataStore<LegacyOrder> {
}
@@ -0,0 +1,13 @@
package com.iluwatar.corruption.system.modern;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* The class represents a customer in the modern system.
*/
@Data
@AllArgsConstructor
public class Customer {
private String address;
}
@@ -0,0 +1,20 @@
package com.iluwatar.corruption.system.modern;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* The class represents an order in the modern system.
*/
@Data
@AllArgsConstructor
public class ModernOrder {
private String id;
private Customer customer;
private Shipment shipment;
private String extra;
}
@@ -0,0 +1,47 @@
package com.iluwatar.corruption.system.modern;
import com.iluwatar.corruption.system.AntiCorruptionLayer;
import com.iluwatar.corruption.system.ShopException;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* The class represents a modern shop system.
* The main purpose of the class is to place orders and find orders.
*/
@Service
public class ModernShop {
@Autowired
private ModernStore store;
@Autowired
private AntiCorruptionLayer acl;
/**
* Places the order in the modern system.
* If the order is already present in the legacy system, then no need to place it again.
*/
public void placeOrder(ModernOrder order) throws ShopException {
String id = order.getId();
// check if the order is already present in the legacy system
Optional<ModernOrder> orderInObsoleteSystem = acl.findOrderInLegacySystem(id);
if (orderInObsoleteSystem.isPresent()) {
var legacyOrder = orderInObsoleteSystem.get();
if (!order.equals(legacyOrder)) {
throw ShopException.throwIncorrectData(legacyOrder.toString(), order.toString());
}
} else {
store.put(id, order);
}
}
/**
* Finds the order in the modern system.
*/
public Optional<ModernOrder> findOrder(String orderId) {
return store.get(orderId);
}
}
@@ -0,0 +1,12 @@
package com.iluwatar.corruption.system.modern;
import com.iluwatar.corruption.system.DataStore;
import org.springframework.stereotype.Service;
/**
* The class represents a data store for the modern system.
*/
@Service
public class ModernStore extends DataStore<ModernOrder> {
}
@@ -0,0 +1,16 @@
package com.iluwatar.corruption.system.modern;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* The class represents a shipment in the modern system.
* The class is used by the modern system to store the data.
*/
@Data
@AllArgsConstructor
public class Shipment {
private String item;
private int qty;
private int price;
}
@@ -0,0 +1,87 @@
package com.iluwatar.corruption.system;
import com.iluwatar.corruption.system.legacy.LegacyOrder;
import com.iluwatar.corruption.system.legacy.LegacyShop;
import com.iluwatar.corruption.system.modern.Customer;
import com.iluwatar.corruption.system.modern.ModernOrder;
import com.iluwatar.corruption.system.modern.ModernShop;
import com.iluwatar.corruption.system.modern.Shipment;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
@RunWith(SpringRunner.class)
@SpringBootTest
public class AntiCorruptionLayerTest {
@Autowired
private LegacyShop legacyShop;
@Autowired
private ModernShop modernShop;
/**
* Test the anti-corruption layer.
* Main intention is to demonstrate how the anti-corruption layer works.
* <p>
* The 2 shops (modern and legacy) should operate independently and in the same time synchronize the data.
* To avoid corrupting the domain models of the 2 shops, we use an anti-corruption layer
* that transforms one model to another under the hood.
*
*/
@Test
public void antiCorruptionLayerTest() throws ShopException {
// a new order comes to the legacy shop.
LegacyOrder legacyOrder = new LegacyOrder("1", "addr1", "item1", 1, 1);
// place the order in the legacy shop.
legacyShop.placeOrder(legacyOrder);
// the order is placed as usual since there is no other orders with the id in the both systems.
Optional<LegacyOrder> legacyOrderWithIdOne = legacyShop.findOrder("1");
assertEquals(Optional.of(legacyOrder), legacyOrderWithIdOne);
// a new order (or maybe just the same order) appears in the modern shop.
ModernOrder modernOrder = new ModernOrder("1", new Customer("addr1"), new Shipment("item1", 1, 1), "");
// the system places it, but it checks if there is an order with the same id in the legacy shop.
modernShop.placeOrder(modernOrder);
Optional<ModernOrder> modernOrderWithIdOne = modernShop.findOrder("1");
// there is no new order placed since there is already an order with the same id in the legacy shop.
assertTrue(modernOrderWithIdOne.isEmpty());
}
/**
* Test the anti-corruption layer.
* Main intention is to demonstrate how the anti-corruption layer works.
* <p>
* This test tests the anti-corruption layer from the rule the orders should be the same in the both systems.
*
*/
@Test(expected = ShopException.class)
public void antiCorruptionLayerWithExTest() throws ShopException {
// a new order comes to the legacy shop.
LegacyOrder legacyOrder = new LegacyOrder("1", "addr1", "item1", 1, 1);
// place the order in the legacy shop.
legacyShop.placeOrder(legacyOrder);
// the order is placed as usual since there is no other orders with the id in the both systems.
Optional<LegacyOrder> legacyOrderWithIdOne = legacyShop.findOrder("1");
assertEquals(Optional.of(legacyOrder), legacyOrderWithIdOne);
// a new order but with the same id and different data appears in the modern shop
ModernOrder modernOrder = new ModernOrder("1", new Customer("addr1"), new Shipment("item1", 10, 1), "");
// the system rejects the order since there are 2 orders with contradiction there.
modernShop.placeOrder(modernOrder);
}
}
+1
View File
@@ -209,6 +209,7 @@
<module>optimistic-offline-lock</module>
<module>crtp</module>
<module>log-aggregation</module>
<module>anti-corruption-layer</module>
<module>health-check</module>
<module>notification</module>
<module>single-table-inheritance</module>