diff --git a/layers/etc/layers.png b/layers/etc/layers.png new file mode 100644 index 000000000..a4bd8b19d Binary files /dev/null and b/layers/etc/layers.png differ diff --git a/layers/etc/layers.ucls b/layers/etc/layers.ucls new file mode 100644 index 000000000..060b391c0 --- /dev/null +++ b/layers/etc/layers.ucls @@ -0,0 +1,256 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/layers/index.md b/layers/index.md new file mode 100644 index 000000000..d97406672 --- /dev/null +++ b/layers/index.md @@ -0,0 +1,19 @@ +--- +layout: pattern +title: Layers +folder: layers +permalink: /patterns/layers/ +categories: pattern_cat +tags: pattern_tag +--- + +**Intent:** Layers is an architectural style where software responsibilities are + divided among the different layers of the application. + +![alt text](./etc/layers.png "Layers") + +**Applicability:** Use the Layers architecture when + +* you want clearly divide software responsibilities into differents parts of the program +* you want to prevent a change from propagating throughout the application +* you want to make your application more maintainable and testable diff --git a/layers/pom.xml b/layers/pom.xml new file mode 100644 index 000000000..dd036e74c --- /dev/null +++ b/layers/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.5.0 + + com.iluwatar.layers + layers + + + org.springframework.data + spring-data-jpa + + + org.hibernate + hibernate-entitymanager + + + commons-dbcp + commons-dbcp + + + com.h2database + h2 + + + junit + junit + test + + + diff --git a/layers/src/main/java/com/iluwatar/layers/App.java b/layers/src/main/java/com/iluwatar/layers/App.java new file mode 100644 index 000000000..3ab5e211e --- /dev/null +++ b/layers/src/main/java/com/iluwatar/layers/App.java @@ -0,0 +1,108 @@ +package com.iluwatar.layers; + +import java.util.Arrays; + +/** + * + *

+ * Layers is an architectural style where software responsibilities are + * divided among the different layers of the application. + *

+ * + *

+ * This example demonstrates a traditional 3-layer architecture consisting of data access + * layer, business layer and presentation layer. + *

+ * + *

+ * The data access layer is formed of Spring Data repositories CakeDao, CakeToppingDao and + * CakeLayerDao. The repositories can be used for CRUD operations on cakes, cake toppings + * and cake layers respectively. + *

+ * + *

+ * The business layer is built on top of the data access layer. CakeBakingService offers + * methods to retrieve available cake toppings and cake layers and baked cakes. Also the + * service is used to create new cakes out of cake toppings and cake layers. + *

+ * + *

+ * The presentation layer is built on the business layer and in this example it simply lists + * the cakes that have been baked. + *

+ * + *

+ * We have applied so called strict layering which means that the layers can only access + * the classes directly beneath them. This leads the solution to create an additional set of + * DTOs (CakeInfo, CakeToppingInfo, CakeLayerInfo) + * to translate data between layers. In other words, CakeBakingService cannot + * return entities (Cake, CakeTopping, CakeLayer) + * directly since these reside on data access layer but instead translates these into business + * layer DTOs (CakeInfo, CakeToppingInfo, CakeLayerInfo) + * and returns them instead. This way the presentation layer does not have any knowledge of + * other layers than the business layer and thus is not affected by changes to them. + *

+ * + * @see Cake + * @see CakeTopping + * @see CakeLayer + * @see CakeDao + * @see CakeToppingDao + * @see CakeLayerDao + * @see CakeBakingService + * @see CakeInfo + * @see CakeToppingInfo + * @see CakeLayerInfo + * + */ +public class App { + + private static CakeBakingService cakeBakingService = new CakeBakingServiceImpl(); + + /** + * Application entry point + * @param args Command line parameters + */ + public static void main(String[] args) { + + // initialize example data + initializeData(cakeBakingService); + + // create view and render it + CakeViewImpl cakeView = new CakeViewImpl(cakeBakingService); + cakeView.render(); + } + + /** + * Initializes the example data + * @param cakeBakingService + */ + private static void initializeData(CakeBakingService cakeBakingService) { + cakeBakingService.saveNewLayer(new CakeLayerInfo("chocolate", 1200)); + cakeBakingService.saveNewLayer(new CakeLayerInfo("banana", 900)); + cakeBakingService.saveNewLayer(new CakeLayerInfo("strawberry", 950)); + cakeBakingService.saveNewLayer(new CakeLayerInfo("lemon", 950)); + cakeBakingService.saveNewLayer(new CakeLayerInfo("vanilla", 950)); + cakeBakingService.saveNewLayer(new CakeLayerInfo("strawberry", 950)); + + cakeBakingService.saveNewTopping(new CakeToppingInfo("candies", 350)); + cakeBakingService.saveNewTopping(new CakeToppingInfo("cherry", 350)); + + CakeInfo cake1 = new CakeInfo(new CakeToppingInfo("candies", 0), + Arrays.asList(new CakeLayerInfo("chocolate", 0), new CakeLayerInfo("banana", 0), + new CakeLayerInfo("strawberry", 0))); + try { + cakeBakingService.bakeNewCake(cake1); + } catch (CakeBakingException e) { + e.printStackTrace(); + } + CakeInfo cake2 = new CakeInfo(new CakeToppingInfo("cherry", 0), + Arrays.asList(new CakeLayerInfo("vanilla", 0), new CakeLayerInfo("lemon", 0), + new CakeLayerInfo("strawberry", 0))); + try { + cakeBakingService.bakeNewCake(cake2); + } catch (CakeBakingException e) { + e.printStackTrace(); + } + } +} diff --git a/layers/src/main/java/com/iluwatar/layers/Cake.java b/layers/src/main/java/com/iluwatar/layers/Cake.java new file mode 100644 index 000000000..193ba5e3f --- /dev/null +++ b/layers/src/main/java/com/iluwatar/layers/Cake.java @@ -0,0 +1,68 @@ +package com.iluwatar.layers; + +import java.util.HashSet; +import java.util.Set; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.OneToOne; + +/** + * + * Cake entity + * + */ +@Entity +public class Cake { + + @Id + @GeneratedValue + private Long id; + + @OneToOne(cascade=CascadeType.REMOVE) + private CakeTopping topping; + + @OneToMany(cascade=CascadeType.REMOVE, fetch=FetchType.EAGER) + private Set layers; + + public Cake() { + setLayers(new HashSet<>()); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public CakeTopping getTopping() { + return topping; + } + + public void setTopping(CakeTopping topping) { + this.topping = topping; + } + + public Set getLayers() { + return layers; + } + + public void setLayers(Set layers) { + this.layers = layers; + } + + public void addLayer(CakeLayer layer) { + this.layers.add(layer); + } + + @Override + public String toString() { + return String.format("id=%s topping=%s layers=%s", id, topping, layers.toString()); + } +} diff --git a/layers/src/main/java/com/iluwatar/layers/CakeBakingException.java b/layers/src/main/java/com/iluwatar/layers/CakeBakingException.java new file mode 100644 index 000000000..0a44d56f9 --- /dev/null +++ b/layers/src/main/java/com/iluwatar/layers/CakeBakingException.java @@ -0,0 +1,18 @@ +package com.iluwatar.layers; + +/** + * + * Custom exception used in cake baking + * + */ +public class CakeBakingException extends Exception { + + private static final long serialVersionUID = 1L; + + public CakeBakingException() { + } + + public CakeBakingException(String message) { + super(message); + } +} diff --git a/layers/src/main/java/com/iluwatar/layers/CakeBakingService.java b/layers/src/main/java/com/iluwatar/layers/CakeBakingService.java new file mode 100644 index 000000000..fec16b494 --- /dev/null +++ b/layers/src/main/java/com/iluwatar/layers/CakeBakingService.java @@ -0,0 +1,48 @@ +package com.iluwatar.layers; + +import java.util.List; + +/** + * + * Service for cake baking operations + * + */ +public interface CakeBakingService { + + /** + * Bakes new cake according to parameters + * @param cakeInfo + * @throws CakeBakingException + */ + void bakeNewCake(CakeInfo cakeInfo) throws CakeBakingException; + + /** + * Get all cakes + * @return + */ + List getAllCakes(); + + /** + * Store new cake topping + * @param toppingInfo + */ + void saveNewTopping(CakeToppingInfo toppingInfo); + + /** + * Get available cake toppings + * @return + */ + List getAvailableToppings(); + + /** + * Add new cake layer + * @param layerInfo + */ + void saveNewLayer(CakeLayerInfo layerInfo); + + /** + * Get available cake layers + * @return + */ + List getAvailableLayers(); +} diff --git a/layers/src/main/java/com/iluwatar/layers/CakeBakingServiceImpl.java b/layers/src/main/java/com/iluwatar/layers/CakeBakingServiceImpl.java new file mode 100644 index 000000000..7e5e3dcff --- /dev/null +++ b/layers/src/main/java/com/iluwatar/layers/CakeBakingServiceImpl.java @@ -0,0 +1,149 @@ +package com.iluwatar.layers; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * + * Implementation of CakeBakingService + * + */ +@Service +@Transactional +public class CakeBakingServiceImpl implements CakeBakingService { + + private AbstractApplicationContext context; + + public CakeBakingServiceImpl() { + this.context = new ClassPathXmlApplicationContext("applicationContext.xml"); + } + + @Override + public void bakeNewCake(CakeInfo cakeInfo) throws CakeBakingException { + List allToppings = getAvailableToppings(); + List matchingToppings = allToppings.stream() + .filter((t) -> t.name.equals(cakeInfo.cakeToppingInfo.name)).collect(Collectors.toList()); + if (matchingToppings.isEmpty()) { + throw new CakeBakingException(String.format("Topping %s is not available", cakeInfo.cakeToppingInfo.name)); + } + List allLayers = getAvailableLayerEntities(); + Set foundLayers = new HashSet<>(); + for (CakeLayerInfo info: cakeInfo.cakeLayerInfos) { + Optional found = allLayers.stream().filter((layer) -> layer.getName().equals(info.name)).findFirst(); + if (!found.isPresent()) { + throw new CakeBakingException(String.format("Layer %s is not available", info.name)); + } else { + foundLayers.add(found.get()); + } + } + CakeToppingDao toppingBean = context.getBean(CakeToppingDao.class); + CakeTopping topping = toppingBean.findOne(matchingToppings.iterator().next().id.get()); + CakeDao cakeBean = context.getBean(CakeDao.class); + Cake cake = new Cake(); + cake.setTopping(topping); + cake.setLayers(foundLayers); + cakeBean.save(cake); + topping.setCake(cake); + toppingBean.save(topping); + CakeLayerDao layerBean = context.getBean(CakeLayerDao.class); + for (CakeLayer layer: foundLayers) { + layer.setCake(cake); + layerBean.save(layer); + } + } + + @Override + public void saveNewTopping(CakeToppingInfo toppingInfo) { + CakeToppingDao bean = context.getBean(CakeToppingDao.class); + bean.save(new CakeTopping(toppingInfo.name, toppingInfo.calories)); + } + + @Override + public void saveNewLayer(CakeLayerInfo layerInfo) { + CakeLayerDao bean = context.getBean(CakeLayerDao.class); + bean.save(new CakeLayer(layerInfo.name, layerInfo.calories)); + } + + private List getAvailableToppingEntities() { + CakeToppingDao bean = context.getBean(CakeToppingDao.class); + List result = new ArrayList<>(); + Iterator iterator = bean.findAll().iterator(); + while (iterator.hasNext()) { + CakeTopping topping = iterator.next(); + if (topping.getCake() == null) { + result.add(topping); + } + } + return result; + } + + @Override + public List getAvailableToppings() { + CakeToppingDao bean = context.getBean(CakeToppingDao.class); + List result = new ArrayList<>(); + Iterator iterator = bean.findAll().iterator(); + while (iterator.hasNext()) { + CakeTopping next = iterator.next(); + if (next.getCake() == null) { + result.add(new CakeToppingInfo(next.getId(), next.getName(), next.getCalories())); + } + } + return result; + } + + private List getAvailableLayerEntities() { + CakeLayerDao bean = context.getBean(CakeLayerDao.class); + List result = new ArrayList<>(); + Iterator iterator = bean.findAll().iterator(); + while (iterator.hasNext()) { + CakeLayer next = iterator.next(); + if (next.getCake() == null) { + result.add(next); + } + } + return result; + } + + @Override + public List getAvailableLayers() { + CakeLayerDao bean = context.getBean(CakeLayerDao.class); + List result = new ArrayList<>(); + Iterator iterator = bean.findAll().iterator(); + while (iterator.hasNext()) { + CakeLayer next = iterator.next(); + if (next.getCake() == null) { + result.add(new CakeLayerInfo(next.getId(), next.getName(), next.getCalories())); + } + } + return result; + } + + @Override + public List getAllCakes() { + CakeDao cakeBean = context.getBean(CakeDao.class); + List result = new ArrayList<>(); + Iterator iterator = cakeBean.findAll().iterator(); + while (iterator.hasNext()) { + Cake cake = iterator.next(); + CakeToppingInfo cakeToppingInfo = new CakeToppingInfo(cake.getTopping().getId(), + cake.getTopping().getName(), cake.getTopping().getCalories()); + ArrayList cakeLayerInfos = new ArrayList(); + for (CakeLayer layer: cake.getLayers()) { + cakeLayerInfos.add(new CakeLayerInfo(layer.getId(), layer.getName(), layer.getCalories())); + } + CakeInfo cakeInfo = new CakeInfo(cake.getId(), cakeToppingInfo, cakeLayerInfos); + result.add(cakeInfo); + } + return result; + } +} diff --git a/layers/src/main/java/com/iluwatar/layers/CakeDao.java b/layers/src/main/java/com/iluwatar/layers/CakeDao.java new file mode 100644 index 000000000..075e75d31 --- /dev/null +++ b/layers/src/main/java/com/iluwatar/layers/CakeDao.java @@ -0,0 +1,14 @@ +package com.iluwatar.layers; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +/** + * + * CRUD repository for cakes + * + */ +@Repository +public interface CakeDao extends CrudRepository { + +} diff --git a/layers/src/main/java/com/iluwatar/layers/CakeInfo.java b/layers/src/main/java/com/iluwatar/layers/CakeInfo.java new file mode 100644 index 000000000..335ce5f4f --- /dev/null +++ b/layers/src/main/java/com/iluwatar/layers/CakeInfo.java @@ -0,0 +1,40 @@ +package com.iluwatar.layers; + +import java.util.List; +import java.util.Optional; + +/** + * + * DTO for cakes + * + */ +public class CakeInfo { + + public final Optional id; + public final CakeToppingInfo cakeToppingInfo; + public final List cakeLayerInfos; + + public CakeInfo(Long id, CakeToppingInfo cakeToppingInfo, List cakeLayerInfos) { + this.id = Optional.of(id); + this.cakeToppingInfo = cakeToppingInfo; + this.cakeLayerInfos = cakeLayerInfos; + } + + public CakeInfo(CakeToppingInfo cakeToppingInfo, List cakeLayerInfos) { + this.id = Optional.empty(); + this.cakeToppingInfo = cakeToppingInfo; + this.cakeLayerInfos = cakeLayerInfos; + } + + public int calculateTotalCalories() { + int total = cakeToppingInfo != null ? cakeToppingInfo.calories : 0; + total += cakeLayerInfos.stream().mapToInt(c -> c.calories).sum(); + return total; + } + + @Override + public String toString() { + return String.format("CakeInfo id=%d topping=%s layers=%s totalCalories=%d", id.get(), cakeToppingInfo, + cakeLayerInfos, calculateTotalCalories()); + } +} diff --git a/layers/src/main/java/com/iluwatar/layers/CakeLayer.java b/layers/src/main/java/com/iluwatar/layers/CakeLayer.java new file mode 100644 index 000000000..2f8649c18 --- /dev/null +++ b/layers/src/main/java/com/iluwatar/layers/CakeLayer.java @@ -0,0 +1,72 @@ +package com.iluwatar.layers; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.ManyToOne; + +/** + * + * CakeLayer entity + * + */ +@Entity +public class CakeLayer { + + @Id + @GeneratedValue + private Long id; + + private String name; + + private int calories; + + @ManyToOne(cascade = CascadeType.ALL) + private Cake cake; + + public CakeLayer() { + } + + public CakeLayer(String name, int calories) { + this.setName(name); + this.setCalories(calories); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getCalories() { + return calories; + } + + public void setCalories(int calories) { + this.calories = calories; + } + + @Override + public String toString() { + return String.format("id=%s name=%s calories=%d", id, name, calories); + } + + public Cake getCake() { + return cake; + } + + public void setCake(Cake cake) { + this.cake = cake; + } +} diff --git a/layers/src/main/java/com/iluwatar/layers/CakeLayerDao.java b/layers/src/main/java/com/iluwatar/layers/CakeLayerDao.java new file mode 100644 index 000000000..9e1d035a8 --- /dev/null +++ b/layers/src/main/java/com/iluwatar/layers/CakeLayerDao.java @@ -0,0 +1,14 @@ +package com.iluwatar.layers; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +/** + * + * CRUD repository for cake layers + * + */ +@Repository +public interface CakeLayerDao extends CrudRepository { + +} diff --git a/layers/src/main/java/com/iluwatar/layers/CakeLayerInfo.java b/layers/src/main/java/com/iluwatar/layers/CakeLayerInfo.java new file mode 100644 index 000000000..9aa7ff7f6 --- /dev/null +++ b/layers/src/main/java/com/iluwatar/layers/CakeLayerInfo.java @@ -0,0 +1,32 @@ +package com.iluwatar.layers; + +import java.util.Optional; + +/** + * + * DTO for cake layers + * + */ +public class CakeLayerInfo { + + public final Optional id; + public final String name; + public final int calories; + + public CakeLayerInfo(Long id, String name, int calories) { + this.id = Optional.of(id); + this.name = name; + this.calories = calories; + } + + public CakeLayerInfo(String name, int calories) { + this.id = Optional.empty(); + this.name = name; + this.calories = calories; + } + + @Override + public String toString() { + return String.format("CakeLayerInfo id=%d name=%s calories=%d", id.get(), name, calories); + } +} diff --git a/layers/src/main/java/com/iluwatar/layers/CakeTopping.java b/layers/src/main/java/com/iluwatar/layers/CakeTopping.java new file mode 100644 index 000000000..f0e30997a --- /dev/null +++ b/layers/src/main/java/com/iluwatar/layers/CakeTopping.java @@ -0,0 +1,72 @@ +package com.iluwatar.layers; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.OneToOne; + +/** + * + * CakeTopping entity + * + */ +@Entity +public class CakeTopping { + + @Id + @GeneratedValue + private Long id; + + private String name; + + private int calories; + + @OneToOne(cascade = CascadeType.ALL) + private Cake cake; + + public CakeTopping() { + } + + public CakeTopping(String name, int calories) { + this.setName(name); + this.setCalories(calories); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getCalories() { + return calories; + } + + public void setCalories(int calories) { + this.calories = calories; + } + + @Override + public String toString() { + return String.format("id=%s name=%s calories=%d", name, calories); + } + + public Cake getCake() { + return cake; + } + + public void setCake(Cake cake) { + this.cake = cake; + } +} diff --git a/layers/src/main/java/com/iluwatar/layers/CakeToppingDao.java b/layers/src/main/java/com/iluwatar/layers/CakeToppingDao.java new file mode 100644 index 000000000..3ddcf53ec --- /dev/null +++ b/layers/src/main/java/com/iluwatar/layers/CakeToppingDao.java @@ -0,0 +1,14 @@ +package com.iluwatar.layers; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +/** + * + * CRUD repository cake toppings + * + */ +@Repository +public interface CakeToppingDao extends CrudRepository { + +} diff --git a/layers/src/main/java/com/iluwatar/layers/CakeToppingInfo.java b/layers/src/main/java/com/iluwatar/layers/CakeToppingInfo.java new file mode 100644 index 000000000..152b0ff85 --- /dev/null +++ b/layers/src/main/java/com/iluwatar/layers/CakeToppingInfo.java @@ -0,0 +1,32 @@ +package com.iluwatar.layers; + +import java.util.Optional; + +/** + * + * DTO for cake toppings + * + */ +public class CakeToppingInfo { + + public final Optional id; + public final String name; + public final int calories; + + public CakeToppingInfo(Long id, String name, int calories) { + this.id = Optional.of(id); + this.name = name; + this.calories = calories; + } + + public CakeToppingInfo(String name, int calories) { + this.id = Optional.empty(); + this.name = name; + this.calories = calories; + } + + @Override + public String toString() { + return String.format("CakeToppingInfo id=%d name=%s calories=%d", id.get(), name, calories); + } +} diff --git a/layers/src/main/java/com/iluwatar/layers/CakeViewImpl.java b/layers/src/main/java/com/iluwatar/layers/CakeViewImpl.java new file mode 100644 index 000000000..5fed15c3a --- /dev/null +++ b/layers/src/main/java/com/iluwatar/layers/CakeViewImpl.java @@ -0,0 +1,19 @@ +package com.iluwatar.layers; + +/** + * + * View implementation for displaying cakes + * + */ +public class CakeViewImpl implements View { + + private CakeBakingService cakeBakingService; + + public CakeViewImpl(CakeBakingService cakeBakingService) { + this.cakeBakingService = cakeBakingService; + } + + public void render() { + cakeBakingService.getAllCakes().stream().forEach((cake) -> System.out.println(cake)); + } +} diff --git a/layers/src/main/java/com/iluwatar/layers/View.java b/layers/src/main/java/com/iluwatar/layers/View.java new file mode 100644 index 000000000..123d4ecbf --- /dev/null +++ b/layers/src/main/java/com/iluwatar/layers/View.java @@ -0,0 +1,12 @@ +package com.iluwatar.layers; + +/** + * + * View interface + * + */ +public interface View { + + void render(); + +} diff --git a/layers/src/main/resources/META-INF/persistence.xml b/layers/src/main/resources/META-INF/persistence.xml new file mode 100644 index 000000000..d94d8582b --- /dev/null +++ b/layers/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/layers/src/main/resources/applicationContext.xml b/layers/src/main/resources/applicationContext.xml new file mode 100644 index 000000000..0c908ad2e --- /dev/null +++ b/layers/src/main/resources/applicationContext.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 1059edc58..de87da567 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,7 @@ business-delegate half-sync-half-async step-builder + layers