You should probably start by separating your concerns into small, extensible pieces that can be easily stitched together and replaced.
interface Transform {
Widget widget(LookupData lookupData);
}
That by itself is a piece
interface Source {
LookupData lookupData();
}
There's your read side effect.
class WidgetMaker {
Source source;
Transform transform;
Widget widget() {
return transform.widget(source.lookupData());
}
}
There's your composition.
class Cache implements Source {
LookupData cache = null;
Source upstream;
LookupData lookupData() {
if (null == cache) {
cache = upstream.lookupData();
}
return cache;
}
}
Now you can put a mini cache in front of any source
... and so on.
it seems you're designing towards injecting the service to fetch the data?
I'm designing toward being able to change my mind. See Parnas 1971.
Injecting immutable data directly into a consumer is going to be my first choice. It's simple, it doesn't entangle the implementation with the imperative shell, which makes the system easier to reason about and easier to test.
But... if the data is something that can be changing during the lifetime of the WidgetMaker, then you have a lot of possible options for how to invalidate your most recently cached copy of the data, and the WidgetMaker itself should be insulated from the strategy that you choose.
When you tell me database, it opens up the possibility that you are needing to access data where the official reference representation is under control of a different process. That's the big scary - needing to access state via the imperative shell, which in the object oriented world means you want to be seriously considering ports and adapters.