It's amazing how many layers you can actually build. It always starts with a good intention - building a sustainable and maintainable architecture. Nothing wrong with that, but the question is how the requirement for sustainability is actually realized. Many J2EE architectures tried to encapsulate every possible technological and functional aspect and variation. Just read the origin DAO-problem statement. A DAO was even intended to encapsulate different data stores like LDAP, flat files, or object oriented databases. But how likely is it, that your project will switch from a relational database to flat file? Rather unlikely for a corporate project, a little bit more likely for a product. The attempt to encapsulate such technology didn't worked very well in the past. Its harder, than you might think to provide a good abstraction. E.g. a DAO worked with detached ("Value Objects") Data Transfer Objects. Every change to these objects, even in the same transaction context, had to be merged back. This is no more necessary in JPA-world. The problem here: often not only the implementation, but the whole paradigm changes. Then your hardly crafted abstraction becomes superfluous. The same can happen with separation of infrastructure/ plumbing code, and domain logic. It was the driving force of many EJB 2.1 applications.EJB 2.X were "polluted" with the infrastructural code, so that you had to build Business Delegates, POJO Facades etc. With the advent of EJB 3 (highly influenced by Hibernate, Ruby On Rails and Spring), all the infrastructural code became hidden behind the framework. Not only the dependency are inverted right now, but especially the patterns and best practices as well. You are basically coding against POJOs, so there is nothing more left to encapsulate... It is hardly possible, actually impossible, to abstract from paradigms.
Another sample is the command pattern. You could implement a command using java.lang.Runnable implementing the run() method, or in distributed environment with javax.jms.MessageListener or MessageDrivenBeans in particular. The problem is: a distributed system hardly behaves like a local one (see: Note On Distributed Computing). So JMS throws a lot of exceptions, is transactional and you have to deal with poisoned messages. So it will be hard to provide a good abstraction for JMS and Threads with the same interface / contract. Such an abstraction will be always leaky.
Knowing that, you have only two choices for building suistainable architectures:
- If you have a lot of experience in technology and domain logic: build good, tight abstractions and encapsulations upfront.
- Try to build as lean as possible architecture and get rid of as many superfluous artifacts (patterns, layers whatever) as only possible. But if you notice, that you are duplicating code, or you are going to be dependent on a specific implementation, provide a good encapsulation and test it. So start with essential things and best practices and let your architecture evolve. It's just common sense...
Both strategies are perfectly valid and can be extremely productive. But if you are not entirely sure about your skills, go with 2 :-). I don't think that, The Premature Optimization Is The Root Of All Evil in Java EE projects is the real problem, rather than too many superfluous layers, tiers, indirections and distributions.
[See also "Real World Java EE Patterns, Rethinking Best Practices" book, Page 253, for more in-depth discussion]
On demand workshops: Java EE 7: Bootstrap, Effective, Testing and Microservices available for streaming.