All field notes

Architecture

Compliance made me a better engineer

4 min readBy Arsh Ramgarhia

I spent a year building fleet software that has to satisfy UK/EU drivers' hours law. It changed how I model data on every project since — including the ones nobody regulates.

For most of my career I treated an audit log the way most people do: a table you add near the end of a sprint, mostly so someone in support can answer 'who changed this?' when a customer complains. Then I started working on a transport management system where a driver's hours record isn't a convenience feature. It's the thing a regulator can ask to see.

UK and EU transport rules put hard limits on how long a driver can be at the wheel, how much rest they need, and how vehicle inspections and defects are recorded. If an operator can't produce those records, that's not a bug report. That's their licence.

That reframing broke a habit I didn't know I had.

UPDATE is a lie you tell yourself

The default way we all model data is that a row represents the current truth. A driver's shift has a start and an end, so you write the start, then later you UPDATE the row with the end. Clean. Obvious. Completely useless when someone asks what the record looked like three weeks ago.

Because the moment you UPDATE, the previous state is gone. Not archived — gone. And 'we don't know' is a bad answer when the question is coming from someone with statutory powers.

So we stopped updating things. A shift correction doesn't overwrite the shift; it appends a new event that supersedes the old one. The current state becomes something you derive, not something you store and mutate.

-- not this
UPDATE driver_shifts SET ended_at = $1 WHERE id = $2;

-- this
INSERT INTO shift_events (shift_id, kind, payload, recorded_at, recorded_by, supersedes)
VALUES ($1, 'shift_ended', $2, now(), $3, $4);

Yes, that's more rows. Storage is cheap and regulators are not.

Every write needs a name attached

The second habit that had to go was anonymous writes. Plenty of systems record what changed without recording who caused it, or they record a service account because the change came through a background job.

If a defect record was dismissed, 'the system did it' is not an acceptable answer. Someone dismissed it, or someone configured the rule that dismissed it automatically. Both of those are people, and both need to be on the record.

  • Who performed the action, as a real user ID — not a service account standing in for one
  • What the state was before and after, not just a diff summary
  • When, in UTC, from the server — never a timestamp the client supplied
  • Why, where the action supports a reason — dismissals and overrides especially

That last one gets pushback every time. Product doesn't want a reason field because it adds friction. But a dismissal without a reason is exactly the record that becomes unexplainable six months later, and friction is the point — you want someone to pause before overriding a compliance alert.

Time is harder than you think

Drivers cross time zones. Rules are written in local time. Tachograph data arrives late, sometimes hours after the event it describes. So you end up needing two different clocks for every event: when it happened, and when you found out about it.

Those come apart more often than you'd expect, and if you only store one, late-arriving data silently corrupts your compliance calculations. A rest period that looked compliant on Tuesday can become non-compliant on Thursday when the tachograph download lands.

If your system can't explain why it thought something was true last week, it doesn't really know anything — it just knows the present.

What I took to everything else

The garage SaaS I work on isn't regulated the way transport is. Nobody is going to pull an operator licence over a mis-edited invoice. I still built the invoice history the same way, because the day a customer disputes a charge, 'here is every version of this invoice and who touched it' ends the conversation in about ninety seconds.

Same with role and permission changes on the volunteer platform I built before that. Grant and revoke are appended, never overwritten. When someone asks why a branch lead could suddenly see national reports, there's an answer with a name and a timestamp on it.

None of this is clever. It's just deciding that history is data, rather than something that falls out of your database as a side effect of the current row.

The regulated project forced me into it. Every project since has been better for it.

ArchitectureAuditingPostgreSQLCompliance
Ready when you are

Let's build something worth opening twice.

From a quiet landing page to a full-blown 3D product, every great build starts with one short conversation. Pick a slot — coffee's on me.