Deleting a user without losing the history: a pattern for B2B platforms
Customers need to remove people. The business needs to know what happened at every order and device. How to give both.
The short answer
Store a pseudonymous identifier on every record that has to outlive the person, set the link to the person to empty when they’re deleted, and make every screen show “Deleted user” with that identifier. The person is gone; the history still makes sense.
Why is deleting a user hard in a B2B platform?
On a smart-locker platform we worked on, customers manage the people who use their lockers: staff, contractors and visitors. People leave, and customers need to remove them. But each person is tied to things the business still needs: click-and-collect orders, the log of which locker was opened when and by whom, and the history behind support questions.
The platform had two bad options. Deleting a person either deleted their orders along with them, losing the history, or was refused because they had orders at all, which meant people who had left could never be removed.
How do you delete a user and keep their history?
- Snapshot an identifier when the record is created. Each order and locker log stores a copy of the person’s user ID alongside the link to the person. We used the ID customers already knew, not a name or an email address.
- Keep the copy in step. If a person’s ID changes while they still exist, their records are updated to match.
- On delete, empty the link instead of deleting the records. The link to the person is set to null, so orders and logs stay, pointing at nobody, with the copied ID still on them.
- Relax validation for finished records. A completed or cancelled order no longer needs a sender or a receiver, so it still saves.
- Show it clearly. Screens show “Deleted user” with the ID, so support staff can still match a log to an old enquiry.
- Backfill existing records. A one-off task copied the ID onto every existing order and log, so the change covered the history too.
class Order < ApplicationRecord
belongs_to :receiver, class_name: "Person", optional: true
before_create { self.receiver_uid = receiver&.uid }
end
class Person < ApplicationRecord
has_many :received_orders, class_name: "Order",
foreign_key: :receiver_id, dependent: :nullify
endWhat about orders that are still in progress?
History is the easy part. An order waiting in a locker for its recipient can’t simply lose its recipient. When a person is deleted, their orders fall into three groups:
- Finished (completed or cancelled): kept, with the person removed.
- Cancellable: cancelled automatically as part of the deletion.
- Genuinely in progress: the deletion is refused with a clear message until the order is finished.
What went wrong after launch?
Three things, all worth planning for:
- Screens that assumed a person. Several pages read the person’s name directly and failed when there was no person. Each was a small fix, but a search for every place a person’s details appear, before launch, would have caught them all.
- Bulk updates skip the search index. Keeping IDs in step used a bulk database update, which is fast but doesn’t run the code that updates the search index. A nightly rebuild of the index reconciled it. If search has to be exact straight away, update records in a way that runs those callbacks.
- Links nobody listed. Later we found that deleting a person should also end their locker allocation. List everything a person can be attached to, not just the obvious tables.
A caution if you’re under GDPR or a similar law: an identifier that can be linked back to a person is still personal data. This pattern keeps operational records usable; it doesn’t make them anonymous. Agree with whoever owns data protection how long the history is kept, and whether the identifier should eventually go too.
Frequently asked questions
Isn’t soft delete simpler?
Soft delete, marking a person as deleted but keeping the row, keeps history easily, but their details stay in your database. When a customer or the law needs the details gone, you need real deletion, and a pattern like this one.
Does this make the data anonymous under GDPR?
No. A pseudonymous ID that can be linked back to someone is still personal data. The pattern reduces what you keep, but you still need a retention policy for the history.
What if a deleted person is added again later?
They come back as a new person with a new link. If they’re given the same ID as before, old records will show it too, so decide whether IDs can be reused at all.