How save() Knows Whether to Insert or Update
The same repository method handles both creating and updating a row, decided by nothing more than whether the object passed in already carries an ID.
Locking every seat means changing its status and its lock timestamp in memory, then actually persisting that change. One repository method handles both writing a brand-new row and updating an existing one.
One Method, Two Outcomes, Decided by an ID
A repository's save method takes an object and returns that same type back, and it does double duty as both a create and an update operation. What decides which one actually happens is nothing more than whether the object passed in already carries an ID.
No ID means there's nothing to check against, so a new row gets created. An ID that already exists means the row is already there, so that row gets updated in place instead. The same rule holds even if an ID gets assigned by custom logic before saving: the object carries whatever ID it was given, and the database itself checks whether a row with that ID already exists to decide which of the two operations actually happens.
Naming a Column So Its Meaning Is Obvious
One small habit worth carrying into this: a column tracking when a lock was applied deserves a name that says exactly that, lockedAt, rather than something more generic like a shared "last updated" timestamp that could mean several different things depending on context. A column's name should make its purpose obvious without needing to check what wrote to it.
The Amount Is Still a Placeholder, on Purpose
Every other field on the ticket gets a real value here, except the amount, deliberately left at zero for now. Summing up each seat's price isn't complicated enough to need its own design, but the actual number a user pays can still differ from that raw sum, once whatever tax applies gets added on top. That's the part worth its own real treatment, and it's picked up separately rather than folded into a quick placeholder here.
Keep reading