diff --git a/game-of-life/src/main/java/fr/ippon/life/Cell.java b/game-of-life/src/main/java/fr/ippon/life/Cell.java
index b8cff6adc6a5d5da330de97001da6ebc35b5b025..91e04656bd13052e94e6ea0ac76b0dbc03370d23 100644
--- a/game-of-life/src/main/java/fr/ippon/life/Cell.java
+++ b/game-of-life/src/main/java/fr/ippon/life/Cell.java
@@ -2,24 +2,16 @@ package fr.ippon.life;
 
 import java.util.Set;
 
-public class Cell {
-
+public record Cell(int row, int column) {   
   private static final int LOW_POPULATION_THRESHOLD = 2;
   private static final int HIGH_POPULATION_THRESHOLD = 3;
   private static final int BORN_THRESHOLD = HIGH_POPULATION_THRESHOLD;
 
-  private final int row;
-  private final int column;
-
-  public Cell(int row, int column) {
-    this.row = row;
-    this.column = column;
-  }
-
   boolean stayAlive(Set<Cell> aliveCells) {
     long neighbourgCount = countNeigbours(aliveCells);
 
-    return neighbourgCount == LOW_POPULATION_THRESHOLD || neighbourgCount == HIGH_POPULATION_THRESHOLD;
+    return neighbourgCount == LOW_POPULATION_THRESHOLD
+        || neighbourgCount == HIGH_POPULATION_THRESHOLD;
   }
 
   boolean born(Set<Cell> aliveCells) {
@@ -27,7 +19,9 @@ public class Cell {
   }
 
   private long countNeigbours(Set<Cell> aliveCells) {
-    return aliveCells.stream().filter(this::isNeighbour).count();
+    return aliveCells.stream()
+        .filter(this::isNeighbour)
+        .count();
   }
 
   private boolean isNeighbour(Cell other) {
@@ -35,38 +29,11 @@ public class Cell {
       return false;
     }
 
-    return delta(row, other.row) <= 1 && delta(column, other.column) <= 1;
+    return delta(row, other.row) <= 1
+        && delta(column, other.column) <= 1;
   }
 
   private static int delta(int first, int second) {
     return Math.abs(first - second);
   }
-
-  public int getRow() {
-    return row;
-  }
-
-  public int getColumn() {
-    return column;
-  }
-
-  @Override
-  public int hashCode() {
-    final int prime = 31;
-    int result = 1;
-    result = prime * result + column;
-    result = prime * result + row;
-    return result;
-  }
-
-  @Override
-  public boolean equals(Object obj) {
-    if (this == obj) return true;
-    if (obj == null) return false;
-    if (getClass() != obj.getClass()) return false;
-    Cell other = (Cell) obj;
-    if (column != other.column) return false;
-    if (row != other.row) return false;
-    return true;
-  }
 }
diff --git a/game-of-life/src/main/java/fr/ippon/life/Grid.java b/game-of-life/src/main/java/fr/ippon/life/Grid.java
index aef8aa67c7462469a87d0f01fd7834c6d47f9010..92ed44436563dc2fde13fcc9f9f00cb14c983465 100644
--- a/game-of-life/src/main/java/fr/ippon/life/Grid.java
+++ b/game-of-life/src/main/java/fr/ippon/life/Grid.java
@@ -9,55 +9,77 @@ import java.util.stream.Stream;
 public class Grid {
 
   private final Set<Cell> deadCells;
-  private final Box box;
+  private final Box border;
 
-  Grid(Set<Cell> cells) {
-    box = new Box(cells);
-    this.deadCells = buildDeadCells(cells);
+  Grid(Set<Cell> aliveCells) {
+    this.deadCells = buildDeadCells(aliveCells);
+    border = new Box(aliveCells);
   }
 
-  private Set<Cell> buildDeadCells(Set<Cell> cells) {
-    return IntStream
-      .range(box.getTopRow(), box.getBottomRow())
-      .mapToObj(Integer::valueOf)
-      .flatMap(row -> toDeadCell(cells, row))
+  private Set<Cell> buildDeadCells(Set<Cell> aliveCells) {
+    return aliveCells
+      .stream()
+      .map(Box::new)
+      .flatMap(Box::cells)
+      .filter(cell -> !aliveCells.contains(cell))
       .collect(Collectors.toUnmodifiableSet());
   }
 
-  private Stream<Cell> toDeadCell(Set<Cell> cells, int row) {
-    return IntStream
-      .range(box.getLeftColumn(), box.getRightColumn())
-      .mapToObj(column -> new Cell(row, column))
-      .filter(cell -> !cells.contains(cell));
-  }
-
   public Collection<Cell> deadCells() {
     return deadCells;
   }
 
+  public int leftColumn() {
+    return border.leftColumn();
+  }
+
+  public int rightColumn() {
+    return border.rightColumn();
+  }
+
+  public int topRow() {
+    return border.topRow();
+  }
+
+  public int bottomRow() {
+    return border.bottomRow();
+  }
+
   private static class Box {
 
     private final Point topLeft;
     private final Point bottomRight;
 
+    public Box(Cell cell) {
+      this(Set.of(cell));
+    }
+
     public Box(Set<Cell> cells) {
       topLeft = Point.topLeft(cells);
       bottomRight = Point.bottomRight(cells);
     }
 
-    public int getTopRow() {
+    private Stream<Cell> cells() {
+      return IntStream.range(leftColumn(), rightColumn()).mapToObj(Integer::valueOf).flatMap(column -> cellsColumn(column));
+    }
+
+    private Stream<Cell> cellsColumn(Integer column) {
+      return IntStream.range(topRow(), bottomRow()).mapToObj(Integer::valueOf).map(row -> new Cell(row, column));
+    }
+
+    public int topRow() {
       return topLeft.getRow();
     }
 
-    public int getBottomRow() {
+    public int bottomRow() {
       return bottomRight.getRow() + 1;
     }
 
-    public int getLeftColumn() {
+    public int leftColumn() {
       return topLeft.getColumn();
     }
 
-    public int getRightColumn() {
+    public int rightColumn() {
       return bottomRight.getColumn() + 1;
     }
   }
@@ -73,17 +95,17 @@ public class Grid {
     }
 
     private static Point topLeft(Set<Cell> cells) {
-      int row = cells.stream().mapToInt(Cell::getRow).min().orElse(0);
+      int row = cells.stream().mapToInt(Cell::row).min().orElse(0);
 
-      int column = cells.stream().mapToInt(Cell::getColumn).min().orElse(0);
+      int column = cells.stream().mapToInt(Cell::column).min().orElse(0);
 
       return new Point(row - 1, column - 1);
     }
 
     private static Point bottomRight(Set<Cell> cells) {
-      int row = cells.stream().mapToInt(Cell::getRow).max().orElse(0);
+      int row = cells.stream().mapToInt(Cell::row).max().orElse(0);
 
-      int column = cells.stream().mapToInt(Cell::getColumn).max().orElse(0);
+      int column = cells.stream().mapToInt(Cell::column).max().orElse(0);
 
       return new Point(row + 1, column + 1);
     }
diff --git a/game-of-life/src/main/java/fr/ippon/life/Renderer.java b/game-of-life/src/main/java/fr/ippon/life/Renderer.java
new file mode 100644
index 0000000000000000000000000000000000000000..ef99786fed395a03bdba78e0699978e862bba3a2
--- /dev/null
+++ b/game-of-life/src/main/java/fr/ippon/life/Renderer.java
@@ -0,0 +1,5 @@
+package fr.ippon.life;
+
+public interface Renderer<T> {
+  T run(Grid grid);
+}
diff --git a/game-of-life/src/main/java/fr/ippon/life/StringRenderer.java b/game-of-life/src/main/java/fr/ippon/life/StringRenderer.java
new file mode 100644
index 0000000000000000000000000000000000000000..03a602d2d405ac0a2166063fc9bd3e76cda1de8c
--- /dev/null
+++ b/game-of-life/src/main/java/fr/ippon/life/StringRenderer.java
@@ -0,0 +1,48 @@
+package fr.ippon.life;
+
+import java.util.function.IntFunction;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+public class StringRenderer implements Renderer<String> {
+
+  @Override
+  public String run(Grid grid) {
+    return new GridRenderer(grid).run();
+  }
+
+  private static class GridRenderer {
+
+    private final Grid grid;
+
+    private GridRenderer(Grid grid) {
+      this.grid = grid;
+    }
+
+    private String run() {
+      return IntStream
+        .range(grid.topRow(), grid.bottomRow())
+        .mapToObj(Integer::valueOf)
+        .map(row -> rowRepresentation(grid, row))
+        .collect(Collectors.joining(System.lineSeparator(), "", System.lineSeparator()));
+    }
+
+    private String rowRepresentation(Grid grid, Integer row) {
+      return IntStream.range(grid.leftColumn(), grid.rightColumn()).mapToObj(cellRepresentation(row)).collect(Collectors.joining());
+    }
+
+    private IntFunction<String> cellRepresentation(Integer row) {
+      return column -> {
+        if (aliveCell(row, column)) {
+          return "o";
+        }
+
+        return "x";
+      };
+    }
+
+    private boolean aliveCell(Integer row, int column) {
+      return !grid.deadCells().contains(new Cell(row, column));
+    }
+  }
+}
diff --git a/game-of-life/src/test/java/fr/ippon/life/GameOfLifeTest.java b/game-of-life/src/test/java/fr/ippon/life/GameOfLifeTest.java
index 799cb41e0666d05ccefa2cb0484afdbf88056af0..8c09185a12d289b18c2b72abed2a1929df347a1d 100644
--- a/game-of-life/src/test/java/fr/ippon/life/GameOfLifeTest.java
+++ b/game-of-life/src/test/java/fr/ippon/life/GameOfLifeTest.java
@@ -39,20 +39,20 @@ class GameOfLifeTest {
 
   @Test
   void nextGenerationShouldHaveOneColumnFromOneLine() {
-    Generation nextGeneration = new Generation(cell(4, 5), cell(5, 5), cell(6, 5)).next();
+    Generation nextGeneration = threeCellsColumn().next();
 
     assertThat(nextGeneration.getAliveCells()).containsExactlyInAnyOrder(cell(5, 4), cell(5, 5), cell(5, 6));
   }
 
   @Test
   void shouldAdvanceInGenerations() {
-    Generation nextGeneration = threeCellsColumn();
+    Generation nextGeneration = threeCellsColumn().next().next();
 
     assertThat(nextGeneration.getAliveCells()).containsExactlyInAnyOrder(cell(4, 5), cell(5, 5), cell(6, 5));
   }
 
   private Generation threeCellsColumn() {
-    return new Generation(cell(4, 5), cell(5, 5), cell(6, 5)).next().next();
+    return new Generation(cell(4, 5), cell(5, 5), cell(6, 5));
   }
 
   private static Cell cell(int row, int column) {
diff --git a/game-of-life/src/test/java/fr/ippon/life/StringRendererUnitTest.java b/game-of-life/src/test/java/fr/ippon/life/StringRendererUnitTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..5afdb183b19accff6ae519d75ee5f3a8092065ad
--- /dev/null
+++ b/game-of-life/src/test/java/fr/ippon/life/StringRendererUnitTest.java
@@ -0,0 +1,47 @@
+package fr.ippon.life;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.junit.jupiter.api.Test;
+
+class StringRendererUnitTest {
+
+  private static final StringRenderer renderer = new StringRenderer();
+
+  @Test
+  void shouldDisplayGridWithOneAliveCell() {
+    assertThat(renderer.run(grid().alive(1, 1).build())).isEqualTo(join("xxx", "xox", "xxx"));
+  }
+
+  @Test
+  void shouldDisplayGridWithTwoAliveCell() {
+    assertThat(renderer.run(grid().alive(1, 1).alive(1, 3).build())).isEqualTo(join("xxxxx", "xoxox", "xxxxx"));
+  }
+
+  private static String join(String... parts) {
+    return Arrays.stream(parts).collect(Collectors.joining(System.lineSeparator(), "", System.lineSeparator()));
+  }
+
+  private GridBuilder grid() {
+    return new GridBuilder();
+  }
+
+  private static class GridBuilder {
+
+    private final Set<Cell> cells = new HashSet<>();
+
+    public GridBuilder alive(int row, int column) {
+      cells.add(new Cell(row, column));
+
+      return this;
+    }
+
+    public Grid build() {
+      return new Grid(cells);
+    }
+  }
+}
diff --git a/package-lock.json b/package-lock.json
index b9bb3235d71a75522bb91f16e439272a50015ec7..c5cef87e66a62a5dbf8dc3a86d35d398efc04181 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1276,8 +1276,7 @@
     "isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
-      "dev": true
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
     },
     "isobject": {
       "version": "3.0.1",
@@ -2093,6 +2092,66 @@
         "java-parser": "1.0.2",
         "lodash": "4.17.21",
         "prettier": "2.2.1"
+      },
+      "dependencies": {
+        "mimic-fn": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+          "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
+        },
+        "npm-run-path": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+          "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+          "requires": {
+            "path-key": "^3.0.0"
+          }
+        },
+        "onetime": {
+          "version": "5.1.2",
+          "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+          "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+          "requires": {
+            "mimic-fn": "^2.1.0"
+          }
+        },
+        "path-key": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+          "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
+        },
+        "prettier": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz",
+          "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==",
+          "dev": true
+        },
+        "shebang-command": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+          "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+          "requires": {
+            "shebang-regex": "^3.0.0"
+          }
+        },
+        "shebang-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+          "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="
+        },
+        "which": {
+          "version": "2.0.2",
+          "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+          "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+          "requires": {
+            "isexe": "^2.0.0"
+          }
+        }
       }
     },
     "pretty-quick": {