Applications evolve. Every developer realizes this fact after a few years of writing code. The trick is to write applications in such a way that there is always room for evolution. While the necessity for code refactor is sometimes inevitable due to several different reasons, applications that have been designed following a few well-known principles are less likely to be completely re-written due to requirement and architecture change.
Portability is the principle that motivates this text. There are definitions of software portability that can be found in different sources, but let us keep it simple and just say that an application is portable if it can be moved from one infrastructure to another with minimal change. For example, a portable rich client application can be converted to a web application by changing the user interface only. A portable web application should be moved from one web server to another with no change to code. To make it possible, the application should not be dependent on the underlying infrastructure code. If that is inevitable, the infrastructure-dependent code should not be allowed to spread across the application; it should be contained behind, say, an interface.
Applications start small, and they should if one would like to minimize risks and deliver a minimum viable product as soon as possible. When designing such applications, architects frequently refer to the the now classic three-tiered model: a front-end tier that contains the user interface code, a back-end application tier that contains the business logic, and a database tier that manages the application’s data. The back-end tier is frequently designed as a three-layered application, with a service façade to communicate with the user interface, a business service layer and a data access layer to interface with the database tier. This model works well when designed properly and, if the tiers and layers are respected, they may be ported to a richer environment as the application grows larger.
With success comes opportunity. A partnership with a third-party service provider may emerge from all the traffic drawn to our application. For instance, someone representing a large company may ask us, “how would you like to see your content on the screens of all subway stations in the city?” Wouldn’t that be sweet? However, there is catch: our partner has its own set of services that need to be integrated with our application. A precedent is set as yet another interested partner is brought in and they also have their own services. Now our well designed application has to communicate with not only its database, but also these inumerous services. What now?
Instead of reaching out to them directly, an integration tier is adopted. All partners communicate with each other through this tier and never directly. The integration tier in turn calls the services exposed by each provider. On the other hand, each service is unique across the environment and is unaware of any other service. A Service Oriented Architecture (SOA) solution is born. And how does SOA help our solution? Through scalability. But scalability is not the word of the day; portability is. So we will leave SOA alone for now.
Let us go back to the beginning and use a more concrete scenario. This time we are designing a Liferay Portal-based application where content, websites, pages and users are maintained. These features are provided out-of-the-box; however, requirements often mandate that they have extra information. On top of that, features not supported by the portal may be requested. Therefore a few extra services are added to the project.
Application developers sometimes regard Liferay as not only a portal and portlet container, but also as a platform, almost an Enterprise Application Server. That is completely understandable. After all, Liferay allows developers to use its security layer, make services join its transactional context, create configurable data sources and more. The motivation for leveraging such infrastructure has already been mentioned here: applications start small, and the facilities offered seem very attractive. The portal even provides the Control Panel as a user interface for many out-of-the-box and custom features. So why not use Liferay as a launching pad?
Liferay recommends the use of its Service Builder to write services deployed in a Liferay environment. The Service Builder is very useful, easy to use and well described in the Developer Guide, but it has a downside. Anyone who has used it probably noticed the dependencies to Liferay code, meaning that custom services are doomed to be only deployable in a Liferay environment. And that is not very portable (this is not entirely true but the code remains dependent on Liferay libraries nevertheless). Liferay-dependent services are okay in a SOA environment because what stays deployed in the portal is only portal-related code such as website and page management. Non-related services are deployed somewhere else. But in our illustrative example we are not there just yet; we are still designing a starter application and our fictitious budget does not support additional infrastructure.
Services are often built to maintain an entity that is defined by the application domain. That means, if our application defines a user entity and its attributes (name, email, address, phone, roles), there should be a service responsible for maintaining these user attributes. Basic operations are often referred to as Create, Retrieve, Update and Delete (CRUD).
A typical service is designed as a three-layered module. It has a service façade layer which can be either a Struts action, a Spring controller, a JSF backing bean or a RESTful web service. The façade invokes the business service layer which contains the business rules. The service layer persists data by either invoking the data access layer, if it is stored locally, or by invoking a service provider interface to communicate with the data host. The whole service module is transactional. If the service only retrieves data and presents it to the client, then the transaction is marked as read-only. It is also secured by the architect’s framework of choice. Services are then packaged as regular Liferay plugins.
The important aspect here is to keep the service code decoupled from the underlying infrastructure. The first thing to keep in mind is to package it as libraries and configure the plugin to import them. Second, these libraries should not have any dependency on Liferay code, even if the application uses out-of-the-box features such as workflow. If that is the case, then wrap the feature invocation with a service provider interface and write the implementation in a separate, Liferay-dependent library.
Our architect came across a few concerns related to infrastructure while designing the application. Fortunately, there are a few tools available that minimize development or make it easier to do. The following aspects were considered. I will only enumerate the concepts behind them and not go into details as they are all described in the Developer Guide, unless stated otherwise:
Security: Register custom entities as Liferay resources by configuring resource-action files. The Control Panel offers a user interface to define permissions on these entities. Most security frameworks validated by the industry offer interfaces for permission evaluation. Provide one that invokes Liferay security services.
Workflow: Register custom Workflow Handlers. The Control Panel offers a user interface to configure workflow definitions to be applied on these entities. Wrap the workflow engine invocations in a service provider interface to facilitate migration.
Transaction: Register a transaction interceptor in an advice chain configured by a service bean auto-proxy creator, defined in the plugin’s Spring context (see kaleo-web source code as an example). Make sure that the chosen transaction attribute source, such as a Transactional annotation, is Liferay-independent. The Spring annotation is recommended.
Liferay out-of-the-box feature maintenance requires synchronization with custom feature: Use service wrappers.
Custom feature maintenance requires synchronization with Liferay feature: Use Aspect Oriented Programming (AOP) advices. Write Liferay service calls in a separate, Liferay-dependent library.
Custom feature is fundamentally related to the Portal and is not expected to change in the foreseeable future: Use Service Builder. Make sure services not dependent on Liferay do not reference Service Builder-generated code.
Custom feature extends existing out-of-the-box feature (example, new page and website attributes): Use Expando Bridge.
I will cover the topics above in more detail in the near future, always from a perspective that takes portability into account.
Applying these techniques will most certainly help scaling solutions to a larger, more diverse environment. While Liferay offers an excellent starting point for small applications, the time may come when most services need to be transferred to external application servers and inter-communication becomes handled by an integration tier such as a service bus. Keeping application and Liferay code decoupled will ease the transition and minimize development time and cost.