Recommended resources
The freely available resources this lesson will direct you to.
- Event Storming - what it is and why you should use it with Domain-Driven Designwww.youtube.com
- Contract First API Design with OpenAPI V3 - Darrel Millerwww.youtube.com
- AIP-121: Resource-oriented designgoogle.aip.dev
- Use Tactical DDD to Design Microservices - Azure Architecture Centerlearn.microsoft.com
Hello. Last lesson, you turned a deliberately incomplete booking brief into questions, assumptions, and observable acceptance criteria. The next move is to give those decisions a shape that a client and an implementer can discuss: a first-pass API contract and a domain sketch. Neither is code, and neither should disguise an unanswered product question as a settled rule.
This is still the blank-repository mission. A useful result today is a small design record you could put beside an empty Go module, review with a stakeholder, and revise without rewriting a service. Allow about 40–45 minutes, including an independent first pass.
Start with the brief, not a framework
Recall the first-version capabilities: a client submits a customer identifier, room identifier, and start and end time to create a booking; a client can retrieve a booking by identifier; clashing reservations should not be accepted. Storage may initially be in memory. Retries, multiple instances, and cancellation all appear in the brief, but their precise first-version promises remain unsettled.
Attempt first — 10 minutes. Close this lesson below this paragraph and draft one page from your requirements notes. Include the two operations, their inputs and observable outcomes, the booking concepts and their relationships, and a separate list of assumptions. If you did not receive answers to your questions from the previous lesson, do not invent confirmation: choose provisional behavior where necessary and label it. Leave Go packages, SQL tables, and handler signatures off the page.
The distinction to preserve is between what a client can rely on and how you might build it. “A successful create returns an identifier that can be retrieved” belongs in a contract. “Store bookings in a map protected by a mutex” does not.
Find the API’s nouns before its routes
Darrel Miller’s Contract First API Design with OpenAPI V3, from NDC Conferences, makes a useful point for this mission: establish use cases before writing a formal API description. The resource sketch comes before detailed URI and schema work.
Contract First API Design with OpenAPI V3 - Darrel Miller
Watch the short transition from requirements to contract-first design, then Miller’s initial resource sketch. The later excerpt shows how a create operation acquires an input body and a created-resource response.
In the opening discussion, watch requirements first: contract-first does not mean starting with specification syntax. Watch resource sketching as he identifies collections and individual resources before settling URI layout. Finally, in the live OpenAPI demonstration, watch create semantics; focus on the distinction between what the client submits and what creation returns, not on YAML syntax.
For the booking brief, Booking is the resource a client creates and retrieves. A Room and a Customer matter to a booking, but the brief does not require endpoints for creating or listing either one. They can appear initially as identifiers referenced by a booking. That choice avoids turning every domain noun into an API operation.
Google’s AIP-121: Resource-oriented design provides a second check: model resources for client use, not as a copy of a prospective database schema.
AIP-121: Resource-oriented design
Read the Google API guidance for its order of design decisions and its warning against making the API mirror storage. Treat its standard-method rules as a design reference, not as a requirement to add operations outside this mission’s scope.
Under “Guidance,” read the “Resources” subsection from the list beginning “When designing an API” through the storage warning. Then read “Methods,” starting at standard methods and continuing through the request/response table. In “Strong Consistency,” find the example beginning “Following a successful create” and read create then get. Notice how that last expectation becomes a client-visible promise you must qualify if the first version stores data only in memory.
AIP-121 generally calls for more standard methods, including List. This brief calls for Create and Get. Adding List just to satisfy a general design convention would enlarge the first slice without evidence that clients need it.
Write a contract someone could challenge
A first-pass contract needs enough detail to expose disagreements: operation, input, successful result, important failure results, and unresolved behavior. It need not yet be OpenAPI YAML. Here is a proposed, not confirmed, contract for the brief:
| Operation | Proposed input | Proposed successful result | Important failure result |
|---|---|---|---|
POST /bookings |
JSON with customerId, roomId, startAt, endAt |
201 Created; booking representation containing a server-assigned id; Location identifies its Get route |
400 for invalid input; 409 for a conflicting booking |
GET /bookings/{id} |
Booking identifier in the path | 200 OK; the booking representation |
404 if no booking with that identifier exists |
For example, the create body could be:
{
"customerId": "customer-17",
"roomId": "room-4",
"startAt": "2026-04-03T09:00:00Z",
"endAt": "2026-04-03T10:00:00Z"
}
A successful response could contain those fields plus "id": "booking-83". A subsequent Get of that identifier should return the same booking data while the same in-memory service instance remains running, assuming no intervening change. The sample identifier illustrates a response; it does not prescribe an ID-generation format. The input omits id because the draft assumes the server assigns it.
The proposed failure categories are useful even before you choose exact error prose:
- Invalid input: a required identifier is empty, a timestamp is unusable, or the end is not later than the start.
- Conflict: another accepted booking for the same room occupies time that this booking would occupy.
- Not found: a requested booking identifier has no matching booking.
A stable machine-readable error code would help clients distinguish these categories; its precise JSON envelope can be agreed separately. More importantly, do not let 409 conceal a retry question. If the first create succeeded but its response was lost, an identical second request might receive a conflict. That response alone would not tell the client whether its first request created the booking. The brief says clients may retry; it does not say whether retries must safely recover the original result. Record that as an open contract decision, rather than quietly claiming idempotency.
The timestamp example also carries an assumption. A draft might require timestamps with an explicit UTC offset and return normalized UTC timestamps. That is a reasonable proposal, not a fact in the brief. Similarly, you might propose that adjacent bookings are allowed when one ends exactly as the next begins. Put both choices in the decision record; later domain work will make the time policy precise.
Sketch the domain without pretending one booking owns the room
The API sketch describes what clients address. A domain sketch describes the concepts and rules needed to decide whether those operations are valid. Microsoft’s Use Tactical DDD to Design Microservices supplies vocabulary for making that distinction.
Use Tactical DDD to Design Microservices - Azure Architecture Center
Read the Azure Architecture Center’s definitions as modeling tools, not as a checklist of classes your Go service must contain. In particular, look for where a rule can live when it concerns one booking versus several bookings.
In “Overview of the tactical patterns,” read “Entities” beginning entity identity; then “Value objects” from value semantics. Read “Aggregates” from consistency boundaries, followed by the “Design small aggregates” and “Reference other aggregates by identity only” rules. Finally, under “Domain and application services,” read from service roles. You can stop before “Domain events”; events are not required to sketch this first slice.
The resource’s diagram shows several possible tactical building blocks. It is not a required request path for this service.

A restrained domain sketch might read:
- Booking: an entity identified by
BookingID, withCustomerID,RoomID, and a time interval. Its identity persists even if two bookings have identical field values. - Interval: a value described by its start and end. Its own validity—such as end later than start—does not require looking up another booking.
- Room and Customer: referenced by identity for this slice. Whether the service must verify that those identifiers exist is still an open requirement.
- Reservation conflict rule: concerns multiple bookings for the same room. One
Bookingobject cannot establish that its proposed interval is free merely by validating itself.
That last point is the important boundary. Calling Booking an aggregate does not magically make it responsible for every other booking for its room. The “no clashes” promise requires a decision against existing bookings and, when requests compete, protection for that decision. For now, name the cross-booking rule and the required observable result. Do not prematurely select the future database transaction or concurrency mechanism.
You can connect the two sketches in plain language: the create operation submits a proposed booking; domain rules assess its fields and whether its room is already occupied for that interval; the result is either a created booking or a classified rejection. This is a responsibility sketch, not an implementation sequence.
Keep a decision record beside the draft
Here is a compact way to keep this proposal honest. Each entry says whether it came from the brief or still needs agreement.
| Item | Status in this draft | Why it matters |
|---|---|---|
| Create and retrieve bookings; reject clashes for the same room | Stated intent | Defines the first slice |
| Server assigns an opaque booking ID; Get uses that ID | Proposed assumption | Determines response and route shape |
| Times include an explicit offset; output is UTC | Proposed assumption | Determines parsing and client interpretation |
| An end time equal to another start time is allowed | Proposed assumption | Determines a conflict boundary |
| Successful create is immediately retrievable from the running instance | Proposed first-version promise | Makes success verifiable, but says nothing about restarts |
| Conflicting simultaneous attempts in one instance accept at most one booking | Proposed clarification of “no clashes” | Makes contention part of the observable contract |
| Whether customer and room IDs must refer to existing records | Open question | Changes validation and dependencies |
| Whether retries recover the original result | Open, high-impact question | Changes what clients can safely do after a lost response |
| Cross-instance coordination, durable storage, cancellation | Not promised by the in-memory first version | Prevents future work from masquerading as current behavior |
This record is part of the design, not an apology attached to it. In particular, a single-instance promise must not be presented as a multi-instance guarantee. Conversely, if stakeholders require safe retries or coordinated reservations in the first demonstration, that answer changes the first-version scope; it cannot be solved by editing error messages.
Before moving on, compare your independent page with this draft. The test is not whether your routes match these examples. Check whether another engineer could identify your client-visible promises, your domain rules, and the decisions you still need—and whether any proposed behavior is accidentally labeled “required.”
A sound first-pass design is small but contestable. You have identified Booking as the created and retrieved resource, separated its intrinsic validity from the rule spanning bookings, and given client-visible outcomes without claiming that unsettled retry, time, or deployment semantics are settled. Keep the draft and its decision record: the next lesson moves into a runnable Go service from a blank repository, where those boundaries can guide implementation rather than be invented inside handlers.