Short answer
Create a small hierarchy rooted at a domain base exception (often unchecked) so callers can catch broadly or specifically, and reserve distinct types for conditions callers will actually branch on.
Explanation
The exception should carry structured context — an error code, the offending id, enough state to act on — not just a string message, so logs and API responses can be machine-readable. Avoid an explosion of one-off exception classes; prefer a few well-typed exceptions carrying fields over dozens of nearly identical types.
Common mistakes
- Creating one custom exception per error with no hierarchy.
- Putting sensitive data in exception messages.