Spec-Driven Development in Practice: Building RoomBook with GitHub Spec Kit
In the first article I stayed on the theory: the specification as a contract, the Spec Kit workflow, a todo list as a toy example. This time I do what I promised and build something real. The application is called RoomBook, a meeting-room booking app with an Angular frontend, a Spring Boot REST backend and SQLite persistence, built from the first line to the last one with GitHub Spec Kit and a coding agent.
The whole project is on my GitHub: room_book on Github. Every spec, plan, checklist and task list I mention below is in the repository, under specs/. I show the method here and link the files for the rest, because a full walkthrough would be longer than anyone wants to read.
I wrote this article with two goals. The first is to show what Spec Kit does, step by step, on a project with real business rules. The second is to be honest about what it gives you and what it does not. As in the first article, this is a personal view, not a verdict.
The Example: RoomBook
Why a room booking app? Because it looks trivial and is not. A booking is just a room, a title, a person and two timestamps, but the rules around it are exactly the kind of thing an AI agent gets subtly wrong when nobody writes them down:
- No overlap on the same room, but back-to-back bookings are allowed (10:00-11:00 and 11:00-12:00).
- Allowed hours 08:00-20:00, Monday to Friday, on 15-minute boundaries.
- Maximum duration 4 hours, and no bookings in the past.
- Cancellation allowed until one hour before the start, as a soft delete.
- A deactivated room cannot be booked, and its future bookings are reported, not deleted.
Authentication, notifications and recurring bookings are explicitly out of scope. Saying what is not in the project is as important as saying what is.
I split the work into five macro features, and each one is a complete Spec Kit cycle:
The first three (setup, rooms API, rooms UI) are the base. Then comes a pure backend feature, the bookings API, and a pure frontend feature, the bookings UI. These two are the interesting ones, because they show how Spec Kit behaves when a project already has a constitution, a contract and a design system, and you only extend them.
This is what the result looks like. The day view on desktop, with several rooms side by side:
The room administration screen, where an inactive room is marked with a label and an icon, not only with a different color:
None of these screens was designed in a mockup tool. They come from a specification, and that is what the rest of the article is about.
Step 0: Init and Constitution
Spec Kit is installed and initialized from the terminal. These are the only terminal commands of the whole process, everything else is typed in the chat of the coding agent:
uv tool install specify-cli
specify init roombook --integration <your-agent>
cd roombook
git init
The repository has to be under Git, because Spec Kit creates numbered feature branches. After the init, the agent gets the Spec Kit commands as skills, and you find them in the project:
<prj_root>/.claude/skills/speckit-specify/SKILL.md
<prj_root>/.claude/skills/speckit-plan/SKILL.md
...
Then the constitution, written once. This is the prompt I used, shortened here (the full text is in the repository):
/speckit-constitution Define principles for RoomBook, a meeting-room booking app with an
Angular frontend, a Spring Boot REST backend and SQLite persistence. Principles:
(1) API-first: the OpenAPI document is the source of truth and is changed before any code.
(2) Contract tests are mandatory for every endpoint.
(3) Business rules live in the service layer, never in controllers or in the frontend.
(4) Time-dependent logic must use an injectable Clock so tests are deterministic.
...
(10) The UI follows one shared design system based on design tokens, defined once and
never hard-coded in components.
The result is a file of eleven principles. Here are a few lines:
<prj_root>/.specify/memory/constitution.md
### I. API-First (NON-NEGOTIABLE)
The OpenAPI document is the single source of truth for the HTTP API. Any API change MUST be
made in the OpenAPI document before any backend or frontend code is written.
### IV. Deterministic Time
All time-dependent logic MUST use an injectable `Clock`. Code MUST NOT call `now()` without a
clock. Tests MUST use a fixed clock so results are deterministic.
I want to stress why this file matters. Every later plan is evaluated against it, and every deviation has to be justified in the plan itself. When the agent writes the booking rules, it does not decide on its own how to handle time: the constitution already decided. That is a piece of architecture, written once, enforced many times.
One Cycle, Five Times
Each macro feature follows the same sequence, which is the one from the first article with the three optional gates switched on:
The cycle is the same every time, and that is the point. I do not invent a new process for a backend feature and another one for a frontend feature. What changes is the content of the prompts: the specification talks about behavior, the plan talks about technology. And I review the output of every step before running the next one.
Let me show the two interesting cases.
Backend Feature: Bookings API
Step 1: Specify (what and why, no technology):
/speckit-specify Let employees book meeting rooms without double bookings. A booking has a
room, a title, the organizer's name, a start and an end time. Two bookings on the same room
cannot overlap, but one may start exactly when another ends. Bookings are allowed from 08:00
to 20:00, Monday to Friday, on 15-minute boundaries, last at most 4 hours and cannot be in the
past. A booking can be cancelled until one hour before it starts; cancelling frees the slot
and keeps the history. A deactivated room cannot be booked...
There is no Spring Boot, no SQL and no endpoint in this text. It describes rules a business person could read and correct.
Step 2: Clarify (find the holes before planning):
/speckit-clarify
This is where I started to see real value. The agent asked targeted questions about things I had not said, and folded the answers back into the spec. These are some of them, copied from the generated file:
<prj_root>/specs/004-bookings-api/spec.md
## Clarifications
- Q: Which time zone applies? -> A: A single office time zone, Europe/Rome by default and
configurable; hours, weekdays and 15-minute boundaries are evaluated in the office time zone.
- Q: Can a booking span midnight? -> A: No; start and end must fall on the same office-local day.
- Q: Is cancellation allowed at exactly one hour before the start? -> A: Yes; it is allowed
when at least 60 minutes remain before the start and refused when fewer remain.
I had never written a word about time zones. Without this step, the agent would have guessed one, silently, and I would have found out in production. With it, the question became a decision with my name on it.
Step 3: Plan (how, with the contract first):
/speckit-plan Extend openapi.yaml first: booking schemas, the four endpoints and the
400/404/409/422 problem+json responses. Add a Flyway migration for the booking table with a
foreign key to room and an index on (room_id, start_at). The booking service validates all
rules using the injectable Clock; overlap check and insert in a single transaction...
Here the technology enters, and the first article's rule applies: the spec says what, the plan says how. Note the order in the prompt: the contract is extended before any code, as the constitution requires. In the generated contract, even the order in which rules are checked is documented:
<prj_root>/specs/001-project-setup/contracts/openapi.yaml
/api/bookings:
post:
operationId: createBooking
description: >
Rules, checked in this order after the fields are present: unknown room (404),
deactivated room (422), hours 08:00-20:00 office time Monday to Friday on 15-minute
boundaries, same local day, duration of at most 4 hours, end after start, start not
before now (400), then overlap with a confirmed booking of the same room (409).
Step 4: Checklist (quality of the requirements, not of the code):
/speckit-checklist Requirements quality for booking rules: completeness, ambiguity and testability of each rule
The checklist produced 42 items. They are questions about the requirements, which is why I find them useful: they read like a reviewer who has not seen the code. For example:
<prj_root>/specs/004-bookings-api/checklists/rules.md
- [ ] CHK005 Is "from 08:00 to 20:00" unambiguous about whether 20:00 is a valid end and 08:00 a valid start?
- [ ] CHK008 Are public holidays explicitly in or out of scope for "Monday to Friday"?
- [ ] CHK016 Is it stated who may cancel a booking, given there is no authentication, and that the organizer name is not a credential?
These checklists are reviewer-owned: the tool generates the questions, a human decides whether the answers are good enough.
Step 5: Tasks, analyze, implement (build it):
/speckit-tasks
/speckit-analyze
/speckit-implement
The task list for this feature has 28 tasks, each one naming the files it touches. /speckit-analyze is read-only: it cross-checks spec, plan and tasks and reports conflicts and gaps before a single line of code is written. Then /speckit-implement executes the tasks in dependency order.
Step 6: Converge (the part I did not expect to like):
/speckit-converge
This was the moment I understood what "checked, not just read" means. After the first implementation, converge compared the code with the spec and found a gap. It added a new task under a Convergence section of tasks.md:
<prj_root>/specs/004-bookings-api/tasks.md
## Phase 9: Convergence
- [X] T028 Add rule tests in `backend/src/test/java/com/roombook/service/BookingRulesTest.java`
with winter offsets and around the daylight-saving change of 25 October 2026 (Europe/Rome):
the same office-local 08:00-20:00 window is accepted before and after the change although
the UTC offset differs, 07:45 and 20:15 are still refused...
The code worked. What was missing was a test for a requirement that came from the clarification about time zones. That is exactly the kind of gap that survives a code review, because nobody thinks about the last Sunday of October on a Tuesday afternoon. Converge did not edit the code: it appended a task, and the loop implement, then converge, closed it.
Frontend Feature: Bookings UI
The frontend feature starts from a very different situation: the backend exists, the contract is published and the design system was created two features earlier. This changes the text of the prompts, and it is where I think Spec Kit shows its structure best.
Specify talks only about the user:
/speckit-specify Let employees see when a meeting room is free and book it from the browser.
They pick a room and a date and see a day view from 08:00 to 20:00 in 15-minute steps, with
booked periods showing the booking title... If the system refuses the booking, the message
explains why in plain language and the entered data is kept. The screens adapt to the device:
on a phone the day is a vertical agenda for one room with a date strip...
Plan names the existing contract as a constraint, not as something to design:
/speckit-plan Angular standalone components with routes /availability and /my-bookings,
reusing the layout, error-display component and generated client from F02. Do not modify
openapi.yaml; the contract from F03 is a fixed constraint... Reuse the design tokens, Material
theme and app shell from F02; add no new colors or fonts.
This sentence is the whole lesson. An agent that is told to "build the booking screens" might decide that a new endpoint would be convenient. An agent that is told the contract is fixed has to find a way inside it, or report a finding. And since the constraint is written down, it can be verified: the feature folder contains a hash of the contract taken before the work started, and the same hash was checked at the end.
<prj_root>/specs/005-bookings-ui/contract-baseline.sha256
664521af719673603dd80717031c4518870d3e9c8f1281633009919aa68353fe
$ sha256sum specs/001-project-setup/contracts/openapi.yaml
664521af719673603dd80717031c4518870d3e9c8f1281633009919aa68353fe
Same hash, same contract. The frontend did not touch it.
The clarify step was again useful for decisions I would have left to chance. Two of them:
<prj_root>/specs/005-bookings-ui/spec.md
- Q: What happens when a selection spans an occupied period? -> A: Only the free part up to
the occupied period is selected; the occupied period is never included.
- Q: How is the organizer name remembered between visits? -> A: It is stored on the device (in
the browser) and pre-filled in the booking form; it is never sent anywhere except as the
organizer of a booking or as the name to look up.
The adaptive behavior is also in the specification, so it is verified like everything else. The same page changes layout at the breakpoints defined in the constitution, 600 and 1024 pixels. On a tablet, a timeline for one room:
On a phone, a vertical agenda with a date strip and a bottom navigation bar:
Here too the convergence step found gaps and wrote them as tasks: component tests for the layout at each breakpoint with a stubbed breakpoint observer, and tests for the date strip and the responsive dialog. Nothing was missing in the screens. What was missing was the proof that they behave as specified.
What Spec Kit Leaves in the Repository
Before looking at the code, it is worth looking at what the process produced next to it. Every macro feature has its own numbered folder under specs/, created by /speckit-specify and filled by the commands that follow:
<prj_root>/
├── .specify/memory/constitution.md <- the eleven principles, written once
├── specs/
│ ├── 001-project-setup/ <- F00, also holds the shared contracts/openapi.yaml
│ ├── 002-rooms-api/ <- F01
│ ├── 003-rooms-ui/ <- F02
│ ├── 004-bookings-api/ <- F03
│ └── 005-bookings-ui/ <- F04
├── backend/
└── frontend/
The five folders have the same shape. This is the one of the bookings API, with the command that created each file:
<prj_root>/specs/004-bookings-api/
├── spec.md <- specify + clarify (what and why, with the Clarifications)
├── plan.md <- plan (how: stack, constraints, structure)
├── research.md <- plan (decisions and rejected alternatives)
├── data-model.md <- plan (entities, fields, constraints)
├── contracts/ <- plan (notes on the changes to the shared openapi.yaml)
├── quickstart.md <- plan (how to run and validate the feature by hand)
├── checklists/
│ ├── requirements.md <- specify (quality of the spec)
│ └── rules.md <- checklist (the 42 questions)
└── tasks.md <- tasks, then updated by implement and converge
The frontend feature 005-bookings-ui has the same files plus two that exist only there: contract-baseline.sha256, the hash of the contract taken before the work, and contrast.md, the check of the colors against the accessibility rules of the constitution.
I like this layout for one reason: each file answers one question, and the question has an owner. spec.md is for the business, plan.md and data-model.md for the architect, checklists/ for the reviewer and tasks.md for whoever runs the implementation. When a rule looks wrong in the code, I know in which file to look for the sentence that caused it.
Mental Model: the specs/ folder is the memory of the project. The code can be regenerated, this folder cannot.
What Was Built: Backend and Frontend
The specs describe the project, and here is what they produced. The two applications are small, and their structure follows the plans closely.
The backend is Spring Boot on Java 21, with SQLite and Flyway:
<prj_root>/backend/src/main/java/com/roombook/
├── api/ BookingController, RoomController, HealthController, DTOs, ApiExceptionHandler
├── service/ BookingService, BookingRules, RoomService, one exception per business rule
├── repository/ BookingRepository, RoomRepository
└── config/ ClockConfig, OfficeZoneConfig, DataDirectoryInitializer
<prj_root>/backend/src/main/resources/db/migration/
├── V2__create_room.sql
├── V3__seed_demo_rooms.sql
└── V4__create_booking.sql
- Layers: controller -> service -> repository, with no business rule in the controllers.
-
Rules in one place:
BookingRulesholds the checks on hours, boundaries and duration, andBookingServiceadds the overlap check and the transaction. -
Errors as types: one exception per rule (
BookingConflictException,CancellationTooLateException,RoomDeactivatedException), mapped to problem+json byApiExceptionHandler, as the contract says. -
Time as a dependency:
ClockConfigandOfficeZoneConfiginject the clock and the office time zone, so the tests can fix both. - Database: one Flyway migration per feature, nothing created by hand.
-
Tests: the
contract/package has one class per group of endpoints and checks the responses againstopenapi.yaml, while theservice/package covers the rules, concurrency and performance.
The frontend is Angular with standalone components and Angular Material:
<prj_root>/frontend/src/app/
├── api/ client generated from openapi.yaml, never edited by hand
├── bookings/ availability.page, day-view, date-strip, booking-form, my-bookings.page, bookings.facade
├── rooms/ room-list.page, room-admin.page, room-card, room-form, deactivate-dialog, rooms.facade
├── layout/ shell and page components (the app shell)
├── shared/ problem-message, responsive-dialog, office-time, clock, organizer-store
└── styles/ tokens.css, theme.scss (the design system)
-
Generated client:
api/comes from the contract, so the frontend cannot call an endpoint that does not exist. - Pages and facades: in the bookings and rooms areas, each page has a facade that wraps the generated client, so components do not talk to HTTP directly.
- Shared pieces: the error component turns a problem+json into a plain message, and the responsive dialog is a dialog on desktop and a full screen on a phone.
-
Design tokens: colors and breakpoints live in
styles/, as the constitution requires, and no feature adds its own.
The counts are small on purpose: 26 backend test classes and 16 frontend spec files, which hold the tests counted below. What matters is that the folders in the code mirror the folders in specs/: a rule in a spec has a place in service/, a screen in a spec has a page in app/.
What the Tool Gives You
After building a real project with it, here is what I would put on the value side:
- Questions before code. Clarify and checklist turn unstated assumptions into written decisions. This is, for me, the single biggest gain: the time zone, the midnight rule and the cancellation boundary were all decided by a person, not by a model.
- Traceability. Every task points to a requirement, every requirement to a user story. When something looks wrong in the code, I can follow it back to a sentence in the spec.
- A constitution that works. Eleven principles, written once, shaped four features. Contract first, fixed clock, problem+json errors: I did not repeat them in a single feature prompt.
- Gates that fail loudly. Converge and analyze do not praise the work, they look for gaps. In my case they found missing tests, which is the kind of debt that normally stays invisible.
- A repeatable cycle. The same eight commands for a backend rule engine and for a responsive grid.
The numbers of the project, for context: 36 tasks for the setup and between 27 and 28 for each of the four features, about 110 backend tests and 91 frontend tests, all generated from the tasks and the contract.
What the Tool Does Not Give You
It would be easy to stop at the list above, so let me add the other side, with the same sincerity as in the first article:
- A wrong spec produces wrong code, faithfully. Converge says the code matches the spec. It cannot say the spec is what the business needs.
- The review work does not disappear, it moves. Reading a constitution, a spec, a plan, a checklist and a task list is real work. I spent more time reading than typing.
- The volume of artifacts is large. Each feature produces a folder with ten files or more. For a one-hour fix, this is too much, and the rule of thumb from the first article applies: use the minimum rigor that removes ambiguity.
- It is spec-anchored, not spec-as-source. The contract and the tests keep the spec honest, but I did not hand the generation over to the spec alone. A person still decides when the loop stops.
Takeaways
If I have to summarize what the practice added to the theory:
- The contract-first chain works in practice. Constitution, spec, plan and OpenAPI constrain each other, and a hash is enough to prove that a frontend feature left the contract untouched.
- The clarify step is where the value starts. It forces the decisions that nobody wants to take upfront, and it records them.
- Converge finds what the tests did not ask for. It does not replace a review, but it makes the gaps visible and turns them into tasks.
- The cost is real. More reading, more files, more discipline. It pays off when the rules are real, as in a booking system, and probably not for a script.
The project, with all the specs, plans, checklists and tasks, is here: . If you clone it, start from specs/004-bookings-api/ and read the clarifications first.
What Comes Next
In the next article I take the challenge I announced: a port of the backend to another technology, changing only the plan. The REST contract, the tests, the data model and the Angular application are my fixed points. RoomBook has now a contract with a known hash and a suite of about 110 backend tests, so the question has become measurable: if they stay untouched, the specification survived the move.
What do you think about Spec Kit on a project with real business rules? Have you tried it, or a different SDD tool? I would love to read your experience in the comments.






Top comments (0)