Skip to content

Part 7 of 7 · Housekeeping dispatcher series ~7 min read

Engineering reference: the housekeeping dispatcher architecture

The first six posts are for the person deciding whether to build this. This one is for the person building it. Same system, no analogies: the services by name, the functions, the two tables, the allocation pass, and the state model.

Key takeaways

  • Single region, single account. Every resource is regional; nothing is global except the IAM roles.
  • 3 Lambda functions, each with its own execution role. No shared role, no wildcards on resources.
  • 2 DynamoDB tables, each keyed so the concurrency story is a condition expression rather than a lock.
  • One Bedrock model, called once, with a JSON schema it must fill or leave null.
  • Nothing always-on: no instance, no container, no provisioned capacity.

The system, by service name

The housekeeping dispatcher drawn with AWS service namesThree boxes across the top outside the AWS account. The property management system providing arrivals and checkouts, read only. Housekeeper phones, receiving the list and sending state changes. And Reception, seeing room states live. Inside the account, three groups. An API for state changes and EventBridge running the morning allocation. Three Lambda functions named allocate, state and balance. And two DynamoDB tables named rooms and workload. A note gives the region as us-east-1, one account, and states that no state is ever set by a timer and do-not-disturb has no override.AWS ACCOUNTThe PMSarrivals, checkouts,read onlyHousekeeper phoneslist and state changesReceptionroom states, liveAPI + EventBridgestate changes,morning allocationLambda x3allocate, state, balanceDynamoDB x2rooms, workloadingroundsoutus-east-1. One account. No state is ever set by a timer; do-not-disturb has no override.
Fig 1. The same shape as Part 1 with the service names filled in. Nothing here is new; it is the same three groups, named.
  • Compute
  • Database
  • Networking
  • Front-end & mobile
  • People

Region and account

  • Region: us-east-1. Chosen because SES inbound receipt rules exist in only a subset of regions and this one has the widest Bedrock model availability. If your data has to stay elsewhere, check both constraints before moving: inbound SES is the binding one.
  • Account: one. This is a small system, and a separate account per environment costs more in wiring than it saves. A dev and a prod stack in the same account, with distinct resource prefixes, is the right size here.
  • Everything is regional. The only global resources are the IAM roles and policies. There is no CloudFront, no global table and no cross-region replication, because nothing here has a latency or durability requirement that would justify them.

Lambda inventory

FunctionTriggerDoesTimeout / memory
hk-allocateEventBridge, each morningGroups by floor, allocates blocks by measured minutes against the four-week balance, orders by required-by time120s / 1024 MB
hk-stateAPI, from the phonesRecords state transitions with a person and a timestamp; creates maintenance jobs10s / 512 MB
hk-balanceEventBridge, nightlyUpdates per-room median durations and each person’s running workload total60s / 512 MB

Splitting this into separate functions is not about modularity. It is that only one of them needs Bedrock permissions and only one is reachable from the public internet, and neither of those is true if it is one handler behind a router.

IAM, scoped

RoleAllowedOn
hk-allocate-roledynamodb:Query, dynamodb:UpdateItemRead-only on the PMS mirror; both tables
hk-state-roledynamodb:UpdateItem, sqs:SendMessageRooms; the maintenance queue
hk-balance-roledynamodb:Query, dynamodb:UpdateItemBoth tables

No role has a Resource: “*” on anything that writes, and every GetSecretValue grant names a single secret arn. That is why there is more than one secret rather than one JSON blob with everything in it.

DynamoDB schemas

Table: rooms

PK   room_id           S   214
     floor             N   2
     state             S   dirty | in_progress | cleaned | inspected | ooo
     state_set_by      S   a person, always; never a timer
     state_set_at      S
     dnd_since         S   set from the door or the PMS; no override exists
     hours_unentered   N   computed; a welfare signal past a threshold
     required_by       S   from the arrival, or null
     work_type         S   checkout | stayover | refresh
     median_minutes    N   across all housekeepers, not one
     requests          L   cot, accessibility, allergy
     flags             L   [{issue, photo_key, at, by}]

`median_minutes` is deliberately person-independent. Using an
individual’s times makes a fast worker permanently own the hard rooms.

Table: workload

PK   person_id         S
SK   date              S
     minutes_allocated N
     minutes_actual    N   from the state transitions
     rooms             N   recorded, but not the allocation unit
     hard_rooms        N   count above a difficulty threshold
     floor             N   for the rotation
     rolling_28d       N   the number the balance is computed on

`rolling_28d` is shown to each person. A fairness total that only a
supervisor can see is a claim rather than a mechanism.

Inbound and outbound

  • Arrivals, checkouts and do-not-disturb come from the property management system, read only. This system writes nothing back to it.
  • State changes come from the phones, one tap, always carrying the person and the time. There is no path that sets a state without a person.
  • Allocation runs once in the morning and adjusts by exception. A list that reorders itself under somebody is not usable.
  • Maintenance flags create jobs in whatever the maintenance process is, not notes in this system. A report that goes nowhere stops being made.

The model call

  • There is no model in this system. Allocation is grouping and sorting; difficulty is a median.
  • The tempting use is predicting when a room will be ready. Part 5 is about why no prediction appears anywhere: it will be quoted to a guest as a promise.
  • A second tempting use is scoring housekeeper productivity. It would make everybody mark rooms inaccurately, which destroys the timing data the fairness depends on.
  • A defensible use is classifying photographed maintenance issues into trades, so the job reaches the right person.
  • The cost page assumes none, which is why the bill is fixed.

Things worth knowing before you build it

  • Allocate by measured minutes, never by room count. Fourteen checkouts and fourteen stayovers differ by about two and a half hours.
  • Group by floor before applying priority. A strictly priority-ordered list costs the better part of an hour per person in movement.
  • Give do-not-disturb no override in any code path. On a busy day an override exists to be used, which is exactly when it should not be.
  • Never set a state from a timer or an inference. One tap from a person, or the state does not change.
  • Publish the rolling workload balance to the team. A fairness figure only the supervisor sees does not produce the trust the system depends on.

That is the whole system. Seven posts, one diagram at a time, and nothing in it that needs a server.

All posts