Engineering reference: the equipment maintenance scheduler 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 two tracks, and how deferrals are stored.
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
- Compute
- Database
- Security & identity
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
devand aprodstack 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
| Function | Trigger | Does | Timeout / memory |
|---|---|---|---|
em-usage | IoT rule or manual API | Appends a usage reading; updates hours since last service | 10s / 512 MB |
em-schedule | EventBridge, nightly | Computes percentage of interval used on both bases; opens jobs at 90% | 120s / 1024 MB |
em-remind | EventBridge, nightly | Runs the statutory ladder from 90 days out; escalates third deferrals; marks lapsed assets out of service | 60s / 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
| Role | Allowed | On |
|---|---|---|
em-usage-role | dynamodb:UpdateItem | Assets only |
em-schedule-role | dynamodb:Query, dynamodb:PutItem | Assets; creates jobs |
em-remind-role | dynamodb:Query, dynamodb:UpdateItem, ses:SendEmail | Both tables; one verified identity |
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: assets
PK asset_id S press_3
basis S hours | cycles | proxy | calendar
interval_units N 500
interval_months N 12 — whichever comes first
units_now N 4640
units_at_service N 4200
last_service_at S
status S in_service | out_of_service
out_of_service_by S statutory_lapse | withdrawn
`basis` appears on every job created, so a proxy-scheduled job is never
mistaken for one scheduled on a measurement.
Table: jobs
PK asset_id S
SK job_id S due_at#type
track S routine | statutory — never merged in a query
due_at S
due_units N 4700
pct_of_interval N 131 — the sort key people should use
deferrals L [{at, reason, note, new_date, by}]
completed_at S
found S replaced parts, or the literal string ’nothing’
certificate_key S statutory jobs only; s3 key
`deferrals` is a list, not a rescheduled due date. The original date and
every subsequent reason survive, which is the whole of Part 3.
Inbound and outbound
- Hour meters report periodically where they can; where they cannot, a weekly manual reading is entered and recorded as manual.
- Jobs open at ninety per cent of interval, on whichever basis applies, which is early enough to arrange and late enough not to clutter.
- Statutory dates come from the last certificate’s examination date, not from when the paperwork arrived.
- Certificates are written once to S3 with object lock and never replaced. A reissued certificate is a new object.
The model call
- There is no model in this system. Everything is counters, intervals and dates.
- The tempting use is failure prediction from usage patterns. At the scale of a small operation there is not enough failure data to learn anything a longer interval and a condition check would not tell you.
- A defensible use is extracting the examination date and any defects from a scanned certificate, with the file kept as the record.
- The wrong use is deciding whether a deferral is acceptable. That is a judgement with a name attached, which is the point of Part 3.
- The cost page assumes none, which is why the bill is almost entirely fixed.
Things worth knowing before you build it
- Store deferrals as a list, never by moving the due date. Rescheduling destroys the evidence that a job has been put off three times.
- Sort overdue work by percentage of interval, not days overdue. Days overdue reliably ranks lightly-used equipment above heavily-used equipment.
- Keep statutory jobs in a separate track with no deferral path in the code, not a flag on a shared list.
- Make ’nothing found’ a recordable completion outcome. Without it, no interval can ever be lengthened on evidence.
- Compute the next statutory date from the examination date on the certificate, not from when the certificate arrived.
That is the whole system. Seven posts, one diagram at a time, and nothing in it that needs a server.
All posts