When using hibernate most important decision you have to make is how you are going to manage sessions. Basically you have to choose from a method scoped (service) session or conversation scoped long running sessions. My choice is here is to use the first one. While using a conversation scoped session might be easier to programmer downside is that database access could happen anywhere on your application uncontrolled. I think that could end you up, having to track down some performance bottlenecks, plus it means having a big object (session) in memory for some time.
Strategy
Not using a conversation scoped session forces you to determine an access strategy for your lazy associations. Note that you should still use these strategies even if you are using conversation scoped session, difference is it doesn't force you to.
Using Join Fetches
Joining usually gets omitted but it's the first thing to do. If you need lazy association, of a list of results, you have to join fetch them. Query below loads the cities with its parks:
select c from City c join fetch c.parks
Of course if you need to list cities which don't have parks you have to use "left join fetch".Caching
I remember reading, probably, on hibernate reference documentation something like caching is the last thing to do for performance. Now I think that I have underestimated using second level cache. Tables that hold rarely changing data should be loaded eagerly and cached. I also use a separate select to load them. This is how I mark such fields;
1: @ManyToOne
2: @Fetch(FetchMode.SELECT)
3: CityType type;
This will force hibernate to cache the data upon first access. Using a cache will generate much smaller queries and you wont have to write huge join statements to load lazy stuff.
I'll probably do more work using cache on an clustered environment.