Pick the Right Exception for the Right Failure
Why a show that doesn't exist and a seat that isn't available deserve two different exception types, when to reuse a built-in exception, and why a value set once should be marked final.
Looking up a show by its ID returns an Optional, the correct place for one to live at all in this codebase. An empty result there means one specific thing has gone wrong, and the exception thrown for it should say exactly that.
A Different Exception for a Different Failure
A show that doesn't exist and a seat that's already taken are two completely different problems, and each one deserves its own exception type, not a shared, generic one. An invalid ID reaching the service is a different failure from a seat that was available a moment ago and now isn't, and a caller catching these exceptions needs to be able to tell them apart.
A custom exception class is worth creating unless a built-in one already matches the exact same purpose, which is rare in practice. Reaching for a generic exception just because one happens to exist makes every failure look the same from the outside, even when the actual causes have nothing in common.
The right grouping is by category of failure, not one exception per field. A show that doesn't exist and a user that doesn't exist are the same kind of problem, an invalid reference to something that should have been real, so the same exception type correctly covers both. A seat that's already taken is a different category entirely, a real business-state conflict rather than a bad reference, which is exactly why it earns an exception of its own.
Mark It Final When It's Only Ever Set Once
One smaller habit worth carrying into this code: a field or parameter whose value gets set once, typically in a constructor, and never changes after that is worth marking as immutable outright. It costs nothing to declare, and it rules out an entire category of bug: nobody, not even by accident, can reassign a value that was only ever meant to be set a single time.
Keep reading