diff --git a/2021-11/spring-29/.gitignore b/2021-11/spring-29/.gitignore
new file mode 100644
index 00000000..4ea52072
--- /dev/null
+++ b/2021-11/spring-29/.gitignore
@@ -0,0 +1,24 @@
+target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/build/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
diff --git a/2021-11/spring-29/spring-29-exercise/pom.xml b/2021-11/spring-29/spring-29-exercise/pom.xml
new file mode 100644
index 00000000..8dc4d8ea
--- /dev/null
+++ b/2021-11/spring-29/spring-29-exercise/pom.xml
@@ -0,0 +1,41 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-29-exercise
+ 1.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.3.3.RELEASE
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-integration
+
+
+ org.springframework
+ spring-messaging
+
+
+ org.apache.commons
+ commons-lang3
+ 3.7
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2021-11/spring-29/spring-29-exercise/src/main/java/ru/otus/spring/integration/App.java b/2021-11/spring-29/spring-29-exercise/src/main/java/ru/otus/spring/integration/App.java
new file mode 100644
index 00000000..15edfe62
--- /dev/null
+++ b/2021-11/spring-29/spring-29-exercise/src/main/java/ru/otus/spring/integration/App.java
@@ -0,0 +1,70 @@
+package ru.otus.spring.integration;
+
+import org.apache.commons.lang3.RandomUtils;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.support.AbstractApplicationContext;
+import org.springframework.integration.annotation.IntegrationComponentScan;
+
+import org.springframework.integration.channel.PublishSubscribeChannel;
+import org.springframework.integration.channel.QueueChannel;
+import org.springframework.integration.config.EnableIntegration;
+import org.springframework.integration.dsl.IntegrationFlow;
+import org.springframework.integration.dsl.IntegrationFlows;
+
+import org.springframework.integration.dsl.MessageChannels;
+import ru.otus.spring.integration.domain.Food;
+import ru.otus.spring.integration.domain.OrderItem;
+
+
+@IntegrationComponentScan
+@SuppressWarnings({"resource", "Duplicates", "InfiniteLoopStatement"})
+@ComponentScan
+@Configuration
+@EnableIntegration
+public class App {
+ private static final String[] MENU = {"coffee", "tea", "smoothie", "whiskey", "beer", "cola", "water"};
+
+ @Bean
+ public QueueChannel itemsChannel() {
+ return MessageChannels.queue(10).get();
+ }
+
+ @Bean
+ public PublishSubscribeChannel foodChannel() {
+ return MessageChannels.publishSubscribe().get();
+ }
+
+ // TODO: create default poller
+
+ @Bean
+ public IntegrationFlow cafeFlow() {
+ return IntegrationFlows.from("itemsChannel")
+ // TODO: cook OrderItem in the kitchen
+ // TODO*: add splitter and aggregator
+ // TODO: forward it to the publish subscriber channel
+ .get();
+ }
+
+ public static void main(String[] args) throws Exception {
+ AbstractApplicationContext ctx = new AnnotationConfigApplicationContext(App.class);
+
+ // here we works with cafe using interface
+ Cafe cafe = ctx.getBean(Cafe.class);
+
+ while (true) {
+ Thread.sleep(1000);
+
+ OrderItem orderItem = generateOrderItem();
+ System.out.println("New orderItem: " + orderItem.getItemName());
+ Food food = cafe.process(orderItem);
+ System.out.println("Ready food: " + food.getName());
+ }
+ }
+
+ private static OrderItem generateOrderItem() {
+ return new OrderItem(MENU[RandomUtils.nextInt(0, MENU.length)]);
+ }
+}
diff --git a/2021-11/spring-29/spring-29-exercise/src/main/java/ru/otus/spring/integration/Cafe.java b/2021-11/spring-29/spring-29-exercise/src/main/java/ru/otus/spring/integration/Cafe.java
new file mode 100644
index 00000000..0114f741
--- /dev/null
+++ b/2021-11/spring-29/spring-29-exercise/src/main/java/ru/otus/spring/integration/Cafe.java
@@ -0,0 +1,12 @@
+package ru.otus.spring.integration;
+
+
+import ru.otus.spring.integration.domain.Food;
+import ru.otus.spring.integration.domain.OrderItem;
+
+// TODO: add messaging gateway annotation
+public interface Cafe {
+
+ // TODO: add gateway annotation with required channels
+ Food process(OrderItem orderItem);
+}
diff --git a/2021-11/spring-29/spring-29-exercise/src/main/java/ru/otus/spring/integration/domain/Food.java b/2021-11/spring-29/spring-29-exercise/src/main/java/ru/otus/spring/integration/domain/Food.java
new file mode 100644
index 00000000..16d8e9c6
--- /dev/null
+++ b/2021-11/spring-29/spring-29-exercise/src/main/java/ru/otus/spring/integration/domain/Food.java
@@ -0,0 +1,15 @@
+package ru.otus.spring.integration.domain;
+
+
+public class Food {
+
+ private final String name;
+
+ public Food(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/2021-11/spring-29/spring-29-exercise/src/main/java/ru/otus/spring/integration/domain/OrderItem.java b/2021-11/spring-29/spring-29-exercise/src/main/java/ru/otus/spring/integration/domain/OrderItem.java
new file mode 100644
index 00000000..68612e96
--- /dev/null
+++ b/2021-11/spring-29/spring-29-exercise/src/main/java/ru/otus/spring/integration/domain/OrderItem.java
@@ -0,0 +1,14 @@
+package ru.otus.spring.integration.domain;
+
+public class OrderItem {
+
+ private final String itemName;
+
+ public OrderItem(String itemName) {
+ this.itemName = itemName;
+ }
+
+ public String getItemName() {
+ return itemName;
+ }
+}
diff --git a/2021-11/spring-29/spring-29-exercise/src/main/java/ru/otus/spring/integration/kitchen/KitchenService.java b/2021-11/spring-29/spring-29-exercise/src/main/java/ru/otus/spring/integration/kitchen/KitchenService.java
new file mode 100644
index 00000000..8e40715f
--- /dev/null
+++ b/2021-11/spring-29/spring-29-exercise/src/main/java/ru/otus/spring/integration/kitchen/KitchenService.java
@@ -0,0 +1,16 @@
+package ru.otus.spring.integration.kitchen;
+
+import org.springframework.stereotype.Service;
+import ru.otus.spring.integration.domain.Food;
+import ru.otus.spring.integration.domain.OrderItem;
+
+@Service
+public class KitchenService {
+
+ public Food cook(OrderItem orderItem) throws Exception {
+ System.out.println("Cooking " + orderItem.getItemName());
+ Thread.sleep(3000);
+ System.out.println("Cooking " + orderItem.getItemName() + " done");
+ return new Food(orderItem.getItemName());
+ }
+}
diff --git a/2021-11/spring-29/spring-29-exercise/src/main/resources/application.yml b/2021-11/spring-29/spring-29-exercise/src/main/resources/application.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/2021-11/spring-29/spring-29-solution/pom.xml b/2021-11/spring-29/spring-29-solution/pom.xml
new file mode 100644
index 00000000..ecdb8292
--- /dev/null
+++ b/2021-11/spring-29/spring-29-solution/pom.xml
@@ -0,0 +1,41 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-29-solution
+ 1.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.3.3.RELEASE
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-integration
+
+
+ org.springframework
+ spring-messaging
+
+
+ org.apache.commons
+ commons-lang3
+ 3.7
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2021-11/spring-29/spring-29-solution/src/main/java/ru/otus/spring/integration/App.java b/2021-11/spring-29/spring-29-solution/src/main/java/ru/otus/spring/integration/App.java
new file mode 100644
index 00000000..bd7a48f2
--- /dev/null
+++ b/2021-11/spring-29/spring-29-solution/src/main/java/ru/otus/spring/integration/App.java
@@ -0,0 +1,96 @@
+package ru.otus.spring.integration;
+
+import org.apache.commons.lang3.RandomUtils;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.support.AbstractApplicationContext;
+import org.springframework.integration.annotation.IntegrationComponentScan;
+import org.springframework.integration.channel.PublishSubscribeChannel;
+import org.springframework.integration.channel.QueueChannel;
+import org.springframework.integration.config.EnableIntegration;
+import org.springframework.integration.dsl.IntegrationFlow;
+import org.springframework.integration.dsl.IntegrationFlows;
+import org.springframework.integration.dsl.MessageChannels;
+import org.springframework.integration.dsl.Pollers;
+import org.springframework.integration.scheduling.PollerMetadata;
+import ru.otus.spring.integration.domain.Food;
+import ru.otus.spring.integration.domain.OrderItem;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.ForkJoinPool;
+import java.util.stream.Collectors;
+
+
+@IntegrationComponentScan
+@SuppressWarnings({ "resource", "Duplicates", "InfiniteLoopStatement" })
+@ComponentScan
+@Configuration
+@EnableIntegration
+public class App {
+ private static final String[] MENU = { "coffee", "tea", "smoothie", "whiskey", "beer", "cola", "water" };
+
+ @Bean
+ public QueueChannel itemsChannel() {
+ return MessageChannels.queue( 10 ).get();
+ }
+
+ @Bean
+ public PublishSubscribeChannel foodChannel() {
+ return MessageChannels.publishSubscribe().get();
+ }
+
+ @Bean(name = PollerMetadata.DEFAULT_POLLER)
+ public PollerMetadata poller() {
+ return Pollers.fixedRate( 100 ).maxMessagesPerPoll( 2 ).get();
+ }
+
+ @Bean
+ public IntegrationFlow cafeFlow() {
+ return IntegrationFlows.from( "itemsChannel" )
+ .split()
+ .handle( "kitchenService", "cook" )
+ .aggregate()
+ .channel( "foodChannel" )
+ .get();
+ }
+
+ public static void main( String[] args ) throws Exception {
+ AbstractApplicationContext ctx = new AnnotationConfigApplicationContext( App.class );
+
+ // here we works with cafe using interface
+ Cafe cafe = ctx.getBean( Cafe.class );
+
+ ForkJoinPool pool = ForkJoinPool.commonPool();
+
+ while ( true ) {
+ Thread.sleep( 7000 );
+
+ pool.execute( () -> {
+ Collection items = generateOrderItems();
+ System.out.println( "New orderItems: " +
+ items.stream().map( OrderItem::getItemName )
+ .collect( Collectors.joining( "," ) ) );
+ Collection food = cafe.process( items );
+ System.out.println( "Ready food: " + food.stream()
+ .map( Food::getName )
+ .collect( Collectors.joining( "," ) ) );
+ } );
+ }
+ }
+
+ private static OrderItem generateOrderItem() {
+ return new OrderItem( MENU[ RandomUtils.nextInt( 0, MENU.length ) ] );
+ }
+
+ private static Collection generateOrderItems() {
+ List items = new ArrayList<>();
+ for ( int i = 0; i < RandomUtils.nextInt( 1, 5 ); ++ i ) {
+ items.add( generateOrderItem() );
+ }
+ return items;
+ }
+}
diff --git a/2021-11/spring-29/spring-29-solution/src/main/java/ru/otus/spring/integration/Cafe.java b/2021-11/spring-29/spring-29-solution/src/main/java/ru/otus/spring/integration/Cafe.java
new file mode 100644
index 00000000..6b31f40a
--- /dev/null
+++ b/2021-11/spring-29/spring-29-solution/src/main/java/ru/otus/spring/integration/Cafe.java
@@ -0,0 +1,16 @@
+package ru.otus.spring.integration;
+
+
+import org.springframework.integration.annotation.Gateway;
+import org.springframework.integration.annotation.MessagingGateway;
+import ru.otus.spring.integration.domain.Food;
+import ru.otus.spring.integration.domain.OrderItem;
+
+import java.util.Collection;
+
+@MessagingGateway
+public interface Cafe {
+
+ @Gateway(requestChannel = "itemsChannel", replyChannel = "foodChannel")
+ Collection process(Collection orderItem);
+}
diff --git a/2021-11/spring-29/spring-29-solution/src/main/java/ru/otus/spring/integration/domain/Food.java b/2021-11/spring-29/spring-29-solution/src/main/java/ru/otus/spring/integration/domain/Food.java
new file mode 100644
index 00000000..16d8e9c6
--- /dev/null
+++ b/2021-11/spring-29/spring-29-solution/src/main/java/ru/otus/spring/integration/domain/Food.java
@@ -0,0 +1,15 @@
+package ru.otus.spring.integration.domain;
+
+
+public class Food {
+
+ private final String name;
+
+ public Food(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/2021-11/spring-29/spring-29-solution/src/main/java/ru/otus/spring/integration/domain/OrderItem.java b/2021-11/spring-29/spring-29-solution/src/main/java/ru/otus/spring/integration/domain/OrderItem.java
new file mode 100644
index 00000000..68612e96
--- /dev/null
+++ b/2021-11/spring-29/spring-29-solution/src/main/java/ru/otus/spring/integration/domain/OrderItem.java
@@ -0,0 +1,14 @@
+package ru.otus.spring.integration.domain;
+
+public class OrderItem {
+
+ private final String itemName;
+
+ public OrderItem(String itemName) {
+ this.itemName = itemName;
+ }
+
+ public String getItemName() {
+ return itemName;
+ }
+}
diff --git a/2021-11/spring-29/spring-29-solution/src/main/java/ru/otus/spring/integration/kitchen/KitchenService.java b/2021-11/spring-29/spring-29-solution/src/main/java/ru/otus/spring/integration/kitchen/KitchenService.java
new file mode 100644
index 00000000..8e40715f
--- /dev/null
+++ b/2021-11/spring-29/spring-29-solution/src/main/java/ru/otus/spring/integration/kitchen/KitchenService.java
@@ -0,0 +1,16 @@
+package ru.otus.spring.integration.kitchen;
+
+import org.springframework.stereotype.Service;
+import ru.otus.spring.integration.domain.Food;
+import ru.otus.spring.integration.domain.OrderItem;
+
+@Service
+public class KitchenService {
+
+ public Food cook(OrderItem orderItem) throws Exception {
+ System.out.println("Cooking " + orderItem.getItemName());
+ Thread.sleep(3000);
+ System.out.println("Cooking " + orderItem.getItemName() + " done");
+ return new Food(orderItem.getItemName());
+ }
+}