diff --git a/README.md b/README.md
index 7db18c7..02ed923 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,7 @@ PHP code katas, tested with [Pest PHP][]. Based on exercises from [Exercism.io][
 Includes:
 
 - **Anagrams**: select an anagram for a word from a list of condidates.
+- **Bob**: returns different responses based on input.
 - **Grade School**: given students' names along with the grade that they are in, create a roster for the school.
 - **Roman numerals**: convert a number into its roman numeral value.
 
diff --git a/src/Bob.php b/src/Bob.php
new file mode 100644
index 0000000..10cce12
--- /dev/null
+++ b/src/Bob.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App;
+
+final class Bob
+{
+    public const RESPONSE_DEFAULT = 'Whatever.';
+    public const RESPONSE_SAY_NOTHING = 'Fine. Be that way!';
+    public const RESPONSE_QUESTION = 'Sure.';
+    public const RESPONSE_YELLING = 'Whoa, chill out!';
+    public const RESPONSE_YELLING_QUESTION = 'Calm down, I know what I\'m doing!';
+
+    private string $input;
+
+    public static function saySomethingTo(string $input): self
+    {
+        return new self($input);
+    }
+
+    public function getResponse(): string
+    {
+        if ($this->input === '') {
+            return self::RESPONSE_SAY_NOTHING;
+        }
+
+        if ($this->isQuestion() && $this->isYelling()) {
+            return self::RESPONSE_YELLING_QUESTION;
+        }
+
+        if ($this->isQuestion()) {
+            return self::RESPONSE_QUESTION;
+        }
+
+        if ($this->isYelling()) {
+            return self::RESPONSE_YELLING;
+        }
+
+        return self::RESPONSE_DEFAULT;
+    }
+
+    final private function __construct(string $input)
+    {
+        $this->input = $input;
+    }
+
+    private function isQuestion(): bool
+    {
+        return (bool) preg_match('/.+\?/', $this->input);
+    }
+
+    private function isYelling(): bool
+    {
+        return (bool) preg_match('/.+!/', $this->input);
+    }
+}
diff --git a/tests/BobTest.php b/tests/BobTest.php
new file mode 100644
index 0000000..1303cf3
--- /dev/null
+++ b/tests/BobTest.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+use App\Bob;
+
+it('replies with a standard response', function (): void {
+    $input = 'Foo bar';
+    $expected = Bob::RESPONSE_DEFAULT;
+
+    assertSame($expected, Bob::saySomethingTo($input)->getResponse());
+});
+
+it('replies if you address him without saying anything', function (): void {
+    $input = '';
+    $expected = Bob::RESPONSE_SAY_NOTHING;
+
+    assertSame($expected, Bob::saySomethingTo($input)->getResponse());
+});
+
+it('replies to a question', function (): void {
+    $input = 'How are you?';
+    $expected = Bob::RESPONSE_QUESTION;
+
+    assertSame($expected, Bob::saySomethingTo($input)->getResponse());
+});
+
+it('replies to yelling', function (): void {
+    $input = 'Go to bed!';
+    $expected = Bob::RESPONSE_YELLING;
+
+    assertSame($expected, Bob::saySomethingTo($input)->getResponse());
+});
+
+it('replies to yelling a question', function (): void {
+    $input = 'Are you OK?!';
+    $expected = Bob::RESPONSE_YELLING_QUESTION;
+
+    assertSame($expected, Bob::saySomethingTo($input)->getResponse());
+});