diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a3769253ae48088095e118e6e7dffb5c43facc51..6be72a4941e93c64fda2dc631ef5860e9eed6ea5 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -36,3 +36,4 @@ include:
   - local: "/mastermind/.gitlab-ci.yml"
   - local: "/try-monade/.gitlab-ci.yml"
   - local: "/java-puzzles/.gitlab-ci.yml"
+  - local: "/java-monades/.gitlab-ci.yml"
diff --git a/java-monades/.gitlab-ci.yml b/java-monades/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..632450ec49a31a04ba95c3f0d2dd1a72b3cafcc1
--- /dev/null
+++ b/java-monades/.gitlab-ci.yml
@@ -0,0 +1,11 @@
+package-java-monades:
+  variables:
+    PROJECT_FOLDER: "java-monades"
+  extends: .java
+  only:
+    refs:
+      - master
+      - merge_requests
+    changes:
+      - ".gitlab-common-ci.yml"
+      - "java-monades/**/*"
diff --git a/java-monades/pom.xml b/java-monades/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9d851f40dc8c47fd1307fcd76b638146259bd79c
--- /dev/null
+++ b/java-monades/pom.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<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">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <version>1.0.0</version>
+    <groupId>fr.ippon.kata</groupId>
+    <artifactId>java-parent</artifactId>
+    <relativePath>../java-parent</relativePath>
+  </parent>
+
+  <version>1.0.0-SNAPSHOT</version>
+  <artifactId>java-monades</artifactId>
+
+  <name>JavaMonades</name>
+
+  <developers>
+    <developer>
+      <email>gautier.difolco@gmail.com</email>
+      <name>Gautier DI FOLCO</name>
+    </developer>
+    <developer>
+      <email>cdamon@ippon.fr</email>
+      <name>Colin DAMON</name>
+    </developer>
+  </developers>
+</project>
diff --git a/java-monades/readme.md b/java-monades/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..55ca076fdc12a7b5ad8cf0c9386bd7f3ba75704d
--- /dev/null
+++ b/java-monades/readme.md
@@ -0,0 +1,9 @@
+# Java monades
+
+Implémentation de différents types de monades en Java
+
+-   **Auteurs** : Gautier DI FOLCO et Colin DAMON
+-   **Date** : 13/05/2021
+-   **Langage** : Java
+-   **Niveau** : Bon Chance
+-   **Replay** : [Twitch](https://www.twitch.tv/videos/1020950472)
diff --git a/java-monades/src/main/java/fr/ippon/monad/Maybe.java b/java-monades/src/main/java/fr/ippon/monad/Maybe.java
new file mode 100644
index 0000000000000000000000000000000000000000..d4ac523420d1683f5e6a69d7515774b443faa08f
--- /dev/null
+++ b/java-monades/src/main/java/fr/ippon/monad/Maybe.java
@@ -0,0 +1,35 @@
+package fr.ippon.monad;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+public class Maybe<T> {
+
+  private final Optional<T> value;
+
+  private Maybe(Optional<T> value) {
+    this.value = value;
+  }
+
+  public static <T> Maybe<T> pure(T value) {
+    return new Maybe<>(Optional.of(value));
+  }
+
+  public static <T> Maybe<T> fail() {
+    return new Maybe<>(Optional.empty());
+  }
+
+  public <U> Maybe<U> map(Function<T, U> mapper) {
+    return new Maybe<>(value.map(mapper));
+  }
+
+  public <U> Maybe<U> bind(Function<T, Maybe<U>> other) {
+    return value.map(other)
+        .orElse(Maybe.fail());
+  }
+
+  @Override
+  public String toString() {
+    return value.toString();
+  }
+}
diff --git a/java-monades/src/main/java/fr/ippon/monad/State.java b/java-monades/src/main/java/fr/ippon/monad/State.java
new file mode 100644
index 0000000000000000000000000000000000000000..d0117112620206be0ca83b12eb0f8e075c060762
--- /dev/null
+++ b/java-monades/src/main/java/fr/ippon/monad/State.java
@@ -0,0 +1,41 @@
+package fr.ippon.monad;
+
+import java.util.function.Function;
+
+public class State<S, T> {
+
+  private final Function<S, Result<S, T>> runner;
+
+  private State(Function<S, Result<S, T>> runner) {
+    this.runner = runner;
+  }
+
+  public static <S, T> State<S, T> pure(T value) {
+    return new State<>(state -> new Result<>(state, value));
+  }
+
+  public static <S> State<S, Void> set(S value) {
+    return new State<>(state -> new Result<>(value, null));
+  }
+
+  public static <S> State<S, S> get() {
+    return new State<>(state -> new Result<>(state, state));
+  }
+
+  public <U> State<S, U> bind(
+      Function<T, State<S, U>> binder) {
+    return new State<>(initial -> {
+      Result<S, T> result = runner.apply(initial);
+
+      return binder.apply(result.result())
+          .run(result.updated());
+    });
+  }
+
+  public Result<S, T> run(S initial) {
+    return runner.apply(initial);
+  }
+
+  public static record Result<S, T> (S updated, T result) {
+  }
+}
diff --git a/java-monades/src/test/java/fr/ippon/monad/MaybeTest.java b/java-monades/src/test/java/fr/ippon/monad/MaybeTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..56f6dca81d5d4d974909c514c7c12371d72ea726
--- /dev/null
+++ b/java-monades/src/test/java/fr/ippon/monad/MaybeTest.java
@@ -0,0 +1,30 @@
+package fr.ippon.monad;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+class MaybeTest {
+
+  @Test
+  void shouldMapFromIntegerToString() {
+    assertThat(Maybe.pure(42)
+        .map(String::valueOf)).usingRecursiveComparison()
+            .isEqualTo(Maybe.pure("42"));
+  }
+
+  @Test
+  void shouldNotMapFailedMaybe() {
+    assertThat(Maybe.fail()
+        .map(String::valueOf)).usingRecursiveComparison()
+            .isEqualTo(Maybe.fail());
+  }
+
+  @Test
+  void shouldBindPureToPure() {
+    assertThat(Maybe.pure(42)
+        .bind(value -> Maybe.pure(String.valueOf(value))))
+            .usingRecursiveComparison()
+            .isEqualTo(Maybe.pure("42"));
+  }
+}
diff --git a/java-monades/src/test/java/fr/ippon/monad/StateTest.java b/java-monades/src/test/java/fr/ippon/monad/StateTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..2854544fc8743281d646fcbe22cb1b3731b05d06
--- /dev/null
+++ b/java-monades/src/test/java/fr/ippon/monad/StateTest.java
@@ -0,0 +1,28 @@
+package fr.ippon.monad;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+class StateTest {
+
+  @Test
+  void shouldWriteSyracuseSequence() {
+    State<Integer, Integer> syracuse = intState()
+        .bind((previous) -> {
+          Integer next = previous % 2 == 0 ? previous / 2
+              : previous * 3 + 1;
+
+          return State.set(next)
+              .bind(dummy -> State.pure(next));
+        });
+
+    assertThat(syracuse.run(5))
+        .isEqualTo(new State.Result<>(16, 16));
+  }
+
+  private State<Integer, Integer> intState() {
+    return State.get();
+  }
+
+}
diff --git a/readme.md b/readme.md
index d83bded7351ce34ff93ee8928c2fffa938b3ff53..bdbf2f26c9b6955f4584873ad7e78351d50d3c95 100644
--- a/readme.md
+++ b/readme.md
@@ -86,6 +86,10 @@ Un kata de code est un petit exercice pensé pour s'entrainer jusqu'à maitriser
 -   [Concurrence en Java](/java-concurrence)
 -   [Try monade](/try-monade)
 
+### Bon Chance
+
+-   [Monades en java](/java-monades)
+
 # Discussions et présentations
 
 -   [C'est une bonne situation ça techlead ?](https://www.youtube.com/watch?v=9tOoXfOE12o)