Brief
You maintain a tiny office utility that runs every morning: it reads the employee list from a flat file and emails a birthday greeting to everyone whose birthday is today. It works, everyone likes it — and it is one class that does everything: file parsing, date logic, greeting text, SMTP. Nobody can test it without a mail server, and the office is about to move the employee list into a database. Your assignment is to refactor it into a hexagon — pure domain core behind ports, technology in adapters — and then prove the boundary by swapping the technology without touching the core.
Instructions
The behavior (unchanged throughout)
- Employees live in
employees.csv, one per line, header included:last_name, first_name, date_of_birth, email, dates asYYYY/MM/DD. - On each run, find every employee whose birthday — month and day of
date_of_birth— matches the run date. - Send each of them an email: subject
Happy birthday!, bodyHappy birthday, dear <first_name>!. - Employees with no birthday today get nothing; an empty match sends nothing.
The starting design (build it or take the kata's)
One class, no seams: it opens the file, parses lines, compares dates, builds the message, and talks to the mail library directly. This flat version is milestone 1 — ugly on purpose.
The extraction assignment
- Build (or adopt) the naive flat version above.
- Pin the behavior with one end-to-end acceptance test at the system edges: a fixture CSV in, a fake SMTP server or captured output out.
- Untangle the pure logic — "who has a birthday on date X" and the greeting content — from all I/O. No interfaces yet.
- Extract two driven ports owned by the core:
EmployeeRepository(provides employees) andGreetingSender(delivers greetings). The CSV reader and the mail sender become adapters implementing them. - Unit-test the core against in-memory fakes; write one contract test per port that both the real adapter and the fake must pass.
- Add a second adapter pair — an SQLite
EmployeeRepositoryand a console or SlackGreetingSender— selected purely by wiring at startup.
Examples
employees.csv:
last_name, first_name, date_of_birth, email
Doe, John, 1982/10/08, john.doe@foobar.com
Ann, Mary, 1975/09/11, mary.ann@foobar.com
A run dated October 8 sends exactly one email:
To: john.doe@foobar.com
Subject: Happy birthday!
Happy birthday, dear John!
A run dated October 9 sends nothing.
Constraints
- Behavior stays identical through every step — the acceptance test from step 2 must pass unchanged from milestone 2 onward.
- After step 4, the core imports nothing from infrastructure: no file, SQL, or mail types cross the ports; the interfaces speak domain language (employees, greetings).
- The final swap (step 6) may only touch adapter code and wiring — a single changed line in the core means the boundary is a fiction.
- Hand-roll the fakes; no mocking framework needed at this size.
Acceptance
Mapped to the exercise's milestones:
- The flat version reproduces the example above end to end.
- The acceptance test runs the whole program against a fixture CSV and asserts on the captured outgoing messages — and fails if the greeting text or recipients change.
- The birthday rule and greeting content are callable with plain values — no file or mail object in sight.
EmployeeRepositoryandGreetingSenderare defined in the core's package/module; dependency arrows all point inward.- Core unit tests run without touching disk or network; each port has a contract test green against both its real adapter and its fake.
- Switching CSV→SQLite and SMTP→console is a wiring-only change with zero core diffs, and the acceptance test still passes.
Related
- Birthday Greetings — hexagonal extraction — the exercise this is the subject of.
- Matteo Vaccari — the Birthday Greetings kata — the original statement this is adapted from.