Skip to content

Part 7 of 7 · Equipment maintenance scheduler series ~7 min read

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

The equipment maintenance scheduler drawn with AWS service namesThree boxes across the top outside the AWS account. Hour meters and manual readings. Schedules, from the manufacturer and from statute. And Certificates, stored immutably. Inside the account, three groups. S3 holding certificates alongside EventBridge running a nightly due pass. Three Lambda functions named usage, schedule and remind. And two DynamoDB tables named assets and jobs. A note gives the region as us-east-1, one account, and states that statutory jobs have no deferral path in any code path or role.AWS ACCOUNTHour metersand manual readingsSchedulesmanufacturer and statutoryCertificatesstored immutablyS3 + EventBridgecertificates,nightly due passLambda x3usage, schedule, remindDynamoDB x2assets, jobsingroundsoutus-east-1. One account. Statutory jobs have no deferral path in any code path or role.
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
  • 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 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
em-usageIoT rule or manual APIAppends a usage reading; updates hours since last service10s / 512 MB
em-scheduleEventBridge, nightlyComputes percentage of interval used on both bases; opens jobs at 90%120s / 1024 MB
em-remindEventBridge, nightlyRuns the statutory ladder from 90 days out; escalates third deferrals; marks lapsed assets out of service60s / 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
em-usage-roledynamodb:UpdateItemAssets only
em-schedule-roledynamodb:Query, dynamodb:PutItemAssets; creates jobs
em-remind-roledynamodb:Query, dynamodb:UpdateItem, ses:SendEmailBoth 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