Thinking differently between frameworks with concrete abstractions (ZIO in Scala) and those with algebras and typeclasses (Haskell / cats & cats-effects in Scala) :-
Using Haskell or Scala cats / cats-effect, which offers libraries based on algebras and typeclasses, we think in terms of the algebra and the appropriate typeclass to model the effect. It's always algebra first - in the following example, we fetch an entity to work with in a domain model. We try fetching from the cache first and if we get a cache miss we reach out for a database based query. With Haskell or cats in Scala, the mantra is using the algebra of an Alternative
typeclass.
getAccount = runMaybeT $
MaybeT (AC.fetchCachedAccount ano)
<|> MaybeT (AR.queryAccount ano)
However, using a framework like ZIO, we tend to look for concrete abstractions and combinators that can nicely compose together to model the effect. ZIO has lots of combinators published specifically for this purpose - you just need to know them and pick the right combination. Less of an algebraic thinking but I think it's definitely faster to get going for someone not familiar with the algebraic thinking or the various typeclasses.
trait Account
def fetchCachedAccount(accountNo: String): Task[Option[Account]] = ???
def queryAccount(accountNo: String): Task[Option[Account]] = ???
def getAccount(ano: String): Task[Option[Account]] =
fetchCachedAccount(ano).some
.orElseOptional(queryAccount(ano).some)
.unsome
Both approaches have their pros and cons - use the one that best fits your team composition.