diff --git a/part_3/exercise_3/.gitignore b/part_3/exercise_3/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..d578053fb6c37346e61ef9da56c648dac284bc98
--- /dev/null
+++ b/part_3/exercise_3/.gitignore
@@ -0,0 +1,30 @@
+### IntelliJ IDEA ###
+/.idea/
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
diff --git a/part_3/exercise_3/exercise_3.iml b/part_3/exercise_3/exercise_3.iml
new file mode 100644
index 0000000000000000000000000000000000000000..c90834f2d607afe55e6104d8aa2cdfffb713f688
--- /dev/null
+++ b/part_3/exercise_3/exercise_3.iml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>
\ No newline at end of file
diff --git a/part_3/exercise_3/src/Circle.java b/part_3/exercise_3/src/Circle.java
new file mode 100644
index 0000000000000000000000000000000000000000..523a2207b2518ab7b78f085ed290909f376f3d4f
--- /dev/null
+++ b/part_3/exercise_3/src/Circle.java
@@ -0,0 +1,33 @@
+import java.util.List;
+
+public class Circle implements ShapeInterface {
+    public static final String NAME = "circle";
+    public static final int NUMBER_OF_POINTS = 2;
+
+    private final List<Point> points;
+
+    public Circle(List<Point> points) {
+        if (points.size() != NUMBER_OF_POINTS) {
+            throw new InvalidShapeException("Circle requires 2 points");
+        }
+
+        this.points = points;
+    }
+
+    @Override
+    public PointBoundary boundaries() {
+        return PointHelper.boundaries(points);
+    }
+
+    /**
+     * area = pi * r^2 ja r = sqrt(dx^2 + dy^2), so area = pi * (dx^2 + dy^2)
+     */
+    @Override
+    public double area() {
+        PointBoundary boundary = this.boundaries();
+        int xLength = boundary.xLength();
+        int yLength = boundary.yLength();
+
+        return Math.PI * (xLength * xLength + yLength * yLength);
+    }
+}
diff --git a/part_3/exercise_3/src/CommandLineApp.java b/part_3/exercise_3/src/CommandLineApp.java
new file mode 100644
index 0000000000000000000000000000000000000000..106f7f22579e966e4fd893e4a8958f1f3f4358c2
--- /dev/null
+++ b/part_3/exercise_3/src/CommandLineApp.java
@@ -0,0 +1,75 @@
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class CommandLineApp {
+    private final CommandLineHelper commandLineHelper;
+    private final ShapeFactory shapeFactory;
+
+    public CommandLineApp() {
+        commandLineHelper = new CommandLineHelper();
+        shapeFactory = new ShapeFactory();
+    }
+
+    public void run() {
+        System.out.println("A circle is defined by a centre and a perimeter point, the others by corner points");
+
+        List<ShapeInterface> shapes = createShapes();
+
+        CompositeShape compositeShape = new CompositeShape(shapes);
+
+        double area = compositeShape.area();
+        PointBoundary boundary = compositeShape.boundaries();
+
+        System.out.println();
+        String output = String.format("Sum of area covered by the patterns:\n%f\n\n", area);
+        output += String.format(
+                "The common boundaries of the patterns:\n(%d, %d) x (%d, %d)",
+                boundary.bottomLeft().x(),
+                boundary.bottomLeft().y(),
+                boundary.topRight().x(),
+                boundary.topRight().y()
+        );
+        commandLineHelper.printSuccessMessage(output);
+    }
+
+    private List<ShapeInterface> createShapes() {
+        ArrayList<ShapeInterface> shapes = new ArrayList<>();
+        Set<String> supportedTypes = shapeFactory.getSupportedTypes();
+        String shapeTypes = String.join(", ", supportedTypes);
+
+        while (true) {
+            String shapeType = commandLineHelper.getStringInput(
+                    String.format("Enter the pattern type (%s) or blank if you have done", shapeTypes),
+                    "Unsupported shape type",
+                    (type) -> type.isEmpty() || shapeFactory.supports(type)
+            );
+
+            if (shapeType.isEmpty()) {
+                break;
+            }
+
+            int numberOfPoints = shapeFactory.getNumberOfPoints(shapeType);
+            List<Point> points = createPoints(numberOfPoints);
+            ShapeInterface shape = shapeFactory.create(shapeType, points);
+            shapes.add(shape);
+        }
+
+        return shapes;
+    }
+
+    private List<Point> createPoints(int numberOfPoints) {
+        ArrayList<Point> points = new ArrayList<>();
+
+        for (int i = 0; i < numberOfPoints; i++) {
+            int x = commandLineHelper.getIntInput("Enter the x-coordinate of the point");
+            int y = commandLineHelper.getIntInput("Enter the y-coordinate of the point");
+            System.out.println();
+
+            Point point = new Point(x, y);
+            points.add(point);
+        }
+
+        return points;
+    }
+}
diff --git a/part_3/exercise_3/src/CommandLineHelper.java b/part_3/exercise_3/src/CommandLineHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..7fa4421a78819d1ce203fbe31d4ceecdba3d329f
--- /dev/null
+++ b/part_3/exercise_3/src/CommandLineHelper.java
@@ -0,0 +1,156 @@
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Scanner;
+import java.util.function.Predicate;
+
+public class CommandLineHelper {
+    private final String ANSI_RESET = "\u001B[0m";
+    private final String ANSI_GREEN = "\u001B[32m";
+    private final String ANSI_RED = "\u001B[31m";
+
+    private final Scanner reader;
+
+    public CommandLineHelper() {
+        reader = new Scanner(System.in);
+    }
+
+    public void printMenu(List<String> strings) {
+        this.printMenu(strings, '*');
+    }
+
+    public void printMenu(List<String> strings, char character) {
+        int maxLen = strings.stream()
+                .mapToInt(String::length)
+                .max()
+                .orElse(0);
+
+        System.out.println(repeatCharacter(character, maxLen + 4));
+
+        for (String str: strings) {
+            System.out.println(
+                    character + " " + str
+                            + repeatCharacter(' ', maxLen - str.length())
+                            + " " + character
+            );
+        }
+
+        System.out.println(repeatCharacter(character, maxLen + 4));
+    }
+
+    public void printMenu(List<String> strings, char character, String color) {
+        System.out.print(color);
+        printMenu(strings, character);
+        System.out.print(ANSI_RESET);
+    }
+
+    public void printMenu(List<String> strings, String color) {
+        this.printMenu(strings, '*', color);
+    }
+
+    public void printMenu(String string, String color) {
+        List<String> messages = new ArrayList<>();
+        messages.add(string);
+
+        printMenu(messages, color);
+    }
+
+    public void printSuccessMenu(String string) {
+        printMenu(string, ANSI_GREEN);
+    }
+
+    public void printSuccessMessage(String string) {
+        System.out.println(ANSI_GREEN + string + ANSI_RESET);
+    }
+
+    public void printErrorMessage(String string) {
+        System.out.println(ANSI_RED + string + ANSI_RESET);
+    }
+
+    public String repeatCharacter(char character, int number) {
+        if (number <= 0) {
+            return "";
+        }
+
+        char[] repeat = new char[number];
+        Arrays.fill(repeat, character);
+
+        return new String(repeat);
+    }
+
+    public void clearScreen() {
+        System.out.print("\033[H\033[2J");
+        System.out.flush();
+    }
+
+    public String getStringInput(String message) {
+        return getStringInput(message, "", null);
+    }
+
+    public String getStringInput(String message, String errorMessage, Predicate<String> tester) {
+        String input;
+
+        while (true) {
+            System.out.print(message + ": ");
+            input = reader.nextLine();
+
+            if (null == tester || tester.test(input)) {
+                break;
+            }
+
+            printErrorMessage(errorMessage);
+        }
+
+        return input;
+    }
+
+    public int getIntInput(String message) {
+        return getIntInput(message, "Error: please enter a number", null);
+    }
+
+    public int getIntInput(String message, String errorMessage, Predicate<Integer> tester) {
+        int input;
+
+        while (true) {
+            try {
+                System.out.print(message + ": ");
+                input = Integer.parseInt(reader.nextLine());
+
+                if (null == tester || tester.test(input)) {
+                    break;
+                }
+
+                printErrorMessage(errorMessage);
+            } catch (NumberFormatException e) {
+                printErrorMessage(errorMessage);
+            }
+        }
+
+        return input;
+    }
+
+    public long getLongInput(String message, Predicate<Long> tester) {
+        return getLongInput(message, "Error: please enter a number", tester);
+    }
+
+    public long getLongInput(String message, String errorMessage, Predicate<Long> tester) {
+        long input;
+
+        while (true) {
+            try {
+                System.out.print(message + ": ");
+                input = Long.parseLong(reader.nextLine());
+
+                if (null == tester || tester.test(input)) {
+                    break;
+                }
+
+                printErrorMessage(errorMessage);
+            } catch (NumberFormatException e) {
+                printErrorMessage(errorMessage);
+            }
+        }
+
+        return input;
+    }
+}
\ No newline at end of file
diff --git a/part_3/exercise_3/src/CompositeShape.java b/part_3/exercise_3/src/CompositeShape.java
new file mode 100644
index 0000000000000000000000000000000000000000..8fd9cf9ceb388096b7db53e25bd2bdbb23a3aa17
--- /dev/null
+++ b/part_3/exercise_3/src/CompositeShape.java
@@ -0,0 +1,34 @@
+import java.util.ArrayList;
+import java.util.List;
+
+public class CompositeShape implements ShapeInterface {
+    private final List<ShapeInterface> shapes;
+
+    public CompositeShape(List<ShapeInterface> shapes) {
+        this.shapes = shapes;
+    }
+
+    @Override
+    public PointBoundary boundaries() {
+        ArrayList<Point> points = new ArrayList<>();
+
+        for (ShapeInterface shape: shapes) {
+            PointBoundary boundary = shape.boundaries();
+            points.add(boundary.bottomLeft());
+            points.add(boundary.topRight());
+        }
+
+        return PointHelper.boundaries(points);
+    }
+
+    @Override
+    public double area() {
+        double sum = 0;
+
+        for (ShapeInterface shape: shapes) {
+            sum += shape.area();
+        }
+
+        return sum;
+    }
+}
diff --git a/part_3/exercise_3/src/Main.java b/part_3/exercise_3/src/Main.java
new file mode 100644
index 0000000000000000000000000000000000000000..df4ad8da61e7487c27c3cf261bf3ffa5e77f929e
--- /dev/null
+++ b/part_3/exercise_3/src/Main.java
@@ -0,0 +1,6 @@
+public class Main {
+    public static void main(String[] args) {
+        CommandLineApp app = new CommandLineApp();
+        app.run();
+    }
+}
\ No newline at end of file
diff --git a/part_3/exercise_3/src/Point.java b/part_3/exercise_3/src/Point.java
new file mode 100644
index 0000000000000000000000000000000000000000..ef22d75926d43f32a2dc8877ec62daadaa04339a
--- /dev/null
+++ b/part_3/exercise_3/src/Point.java
@@ -0,0 +1,2 @@
+public record Point(int x, int y) {
+}
diff --git a/part_3/exercise_3/src/PointBoundary.java b/part_3/exercise_3/src/PointBoundary.java
new file mode 100644
index 0000000000000000000000000000000000000000..1bca27a402ca834b678da91aa05575dc460fcb22
--- /dev/null
+++ b/part_3/exercise_3/src/PointBoundary.java
@@ -0,0 +1,26 @@
+import java.util.Objects;
+
+public record PointBoundary(Point bottomLeft, Point topRight) {
+    public PointBoundary {
+        Objects.requireNonNull(bottomLeft, "Bottom left should not be null");
+        Objects.requireNonNull(topRight, "Top right should not be null");
+
+        if (topRight.x() < bottomLeft.x() || topRight.y() < bottomLeft.y()) {
+            throw new InvalidBoundaryException("Top right X should be greater than or equal bottom left X, top right Y should be greater than or equal bottom left Y");
+        }
+    }
+
+    public int xLength() {
+        return topRight.x() - bottomLeft.x();
+    }
+
+    public int yLength() {
+        return topRight().y() - bottomLeft().y();
+    }
+}
+
+class InvalidBoundaryException extends RuntimeException {
+    public InvalidBoundaryException(String message) {
+        super(message);
+    }
+}
\ No newline at end of file
diff --git a/part_3/exercise_3/src/PointHelper.java b/part_3/exercise_3/src/PointHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..ed93fe78c148fdb9fc71495eb72a8cf39031a5cf
--- /dev/null
+++ b/part_3/exercise_3/src/PointHelper.java
@@ -0,0 +1,32 @@
+import java.util.List;
+
+public class PointHelper {
+    public static PointBoundary boundaries(List<Point> points) {
+        int maxX = Integer.MIN_VALUE,
+                maxY = Integer.MIN_VALUE,
+                minX = Integer.MAX_VALUE,
+                minY = Integer.MAX_VALUE;
+
+        if (points.size() == 0) {
+            maxX = 0;
+            maxY = 0;
+            minX = 0;
+            minY = 0;
+        }
+
+        for (Point point: points) {
+            maxX = Math.max(maxX, point.x());
+            maxY = Math.max(maxY, point.y());
+            minX = Math.min(minX, point.x());
+            minY = Math.min(minY, point.y());
+        }
+
+        assert (maxX >= minX);
+        assert (maxY >= minY);
+
+        Point bottomLeft = new Point(minX, minY);
+        Point topRight = new Point(maxX, maxY);
+
+        return new PointBoundary(bottomLeft, topRight);
+    }
+}
diff --git a/part_3/exercise_3/src/Quadrilateral.java b/part_3/exercise_3/src/Quadrilateral.java
new file mode 100644
index 0000000000000000000000000000000000000000..c1330eed1529c9c1665d5158893ae0bed1e45518
--- /dev/null
+++ b/part_3/exercise_3/src/Quadrilateral.java
@@ -0,0 +1,33 @@
+import java.util.List;
+
+public class Quadrilateral implements ShapeInterface {
+    public static final String NAME = "quadrilateral";
+    public static final int NUMBER_OF_POINTS = 2;
+
+    private final List<Point> points;
+
+    public Quadrilateral(List<Point> points) {
+        if (points.size() != NUMBER_OF_POINTS) {
+            throw new InvalidShapeException("Quadrilateral requires 2 points");
+        }
+
+        this.points = points;
+    }
+
+    @Override
+    public PointBoundary boundaries() {
+        return PointHelper.boundaries(points);
+    }
+
+    /**
+     * area = side * other side
+     */
+    @Override
+    public double area() {
+        PointBoundary boundary = this.boundaries();
+        int xLength = boundary.xLength();
+        int yLength = boundary.yLength();
+
+        return xLength * yLength;
+    }
+}
diff --git a/part_3/exercise_3/src/ShapeFactory.java b/part_3/exercise_3/src/ShapeFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..342092436b16da5fa03424e0e9cbe2c92bbe0d9a
--- /dev/null
+++ b/part_3/exercise_3/src/ShapeFactory.java
@@ -0,0 +1,35 @@
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
+
+public class ShapeFactory {
+    private final HashMap<String, Integer> numberOfPoints;
+
+    public ShapeFactory() {
+        numberOfPoints = new HashMap<>();
+        numberOfPoints.put(Circle.NAME, Circle.NUMBER_OF_POINTS);
+        numberOfPoints.put(Quadrilateral.NAME, Quadrilateral.NUMBER_OF_POINTS);
+        numberOfPoints.put(Triangle.NAME, Triangle.NUMBER_OF_POINTS);
+    }
+
+    public Set<String> getSupportedTypes() {
+        return numberOfPoints.keySet();
+    }
+
+    public boolean supports(String shapeType) {
+        return numberOfPoints.containsKey(shapeType);
+    }
+
+    public int getNumberOfPoints(String shapeType) {
+        return numberOfPoints.get(shapeType);
+    }
+
+    public ShapeInterface create(String shapeType, List<Point> points) {
+        return switch (shapeType) {
+            case Circle.NAME -> new Circle(points);
+            case Quadrilateral.NAME -> new Quadrilateral(points);
+            case Triangle.NAME -> new Triangle(points);
+            default -> throw new RuntimeException("Unsupported shape");
+        };
+    }
+}
diff --git a/part_3/exercise_3/src/ShapeInterface.java b/part_3/exercise_3/src/ShapeInterface.java
new file mode 100644
index 0000000000000000000000000000000000000000..f60af2d7ec5ae8c5c605fa5fa6bbb35ff8d20cfa
--- /dev/null
+++ b/part_3/exercise_3/src/ShapeInterface.java
@@ -0,0 +1,11 @@
+public interface ShapeInterface {
+    PointBoundary boundaries();
+
+    double area();
+}
+
+class InvalidShapeException extends RuntimeException {
+    public InvalidShapeException(String message) {
+        super(message);
+    }
+}
\ No newline at end of file
diff --git a/part_3/exercise_3/src/Triangle.java b/part_3/exercise_3/src/Triangle.java
new file mode 100644
index 0000000000000000000000000000000000000000..6518fd7dd497d540db9c2ec6d7790e51b973cabe
--- /dev/null
+++ b/part_3/exercise_3/src/Triangle.java
@@ -0,0 +1,33 @@
+import java.util.List;
+
+public class Triangle implements ShapeInterface {
+    public static final String NAME = "triangle";
+    public static final int NUMBER_OF_POINTS = 3;
+
+    private final List<Point> points;
+
+    public Triangle(List<Point> points) {
+        if (points.size() != NUMBER_OF_POINTS) {
+            throw new InvalidShapeException("Triangle requires 3 points");
+        }
+
+        this.points = points;
+    }
+
+    @Override
+    public PointBoundary boundaries() {
+        return PointHelper.boundaries(points);
+    }
+
+    /**
+     * Draw a quadrilateral around it and calculate half of the area
+     */
+    @Override
+    public double area() {
+        PointBoundary boundary = this.boundaries();
+        int xLength = boundary.xLength();
+        int yLength = boundary.yLength();
+
+        return (double) (xLength * yLength) / 2;
+    }
+}