Summary
Here are the principles I would extract:
- If you have a reasonable expectation that your code may change, then you should use some kind of run-time dispatch mechanism (even that from "Plain Functions + Client Namespace").
- Using run-time dispatch, even if you don't necessarily need it, will increase the elasticity of your code.
- Most likely, you should have a client namespace to separate client (code that uses the abstraction) concerns from backend (code that implements the abstraction) concerns and limit the surface area for implementation.
- Use a life cycle for components in your application, unless it would always be overkill for every situation that you can possibly imagine.
- If you need the construct a service object based on run-time information (or deploy-time information), consider using the "Protocol + Client + Multimethod" approach.