All articles
Contents

    How to Develop a Highly Customizable Product

    Have you ever heard: 'We really like your product…except for a few minor details.'? And then the CIO rolls out a list of additional 'must have' requirements, hundreds of them, to add to your amazing product. Have you ever heard, or even said: 'Team, we are about to sign up a highly-profitable contract but…'? And then the customer’s wish list for additional functionality becomes a headache for developers.

    So, how can the product be kept a step away from the potentially dangerous ideas of your customers, yet still satisfy them? How can it be possible to maintain the highest levels of performance for a product technically designed to function in a particular way, but now with a layer of numerous add-ins? How much of a challenge will be created by the fundamental need to provide unfailing and outstanding support to the developed solution?

    In the commercial world, product customization is an increasingly desirable requirement and a number of common practices have evolved in response to this customer need. Below you can find an overview of the typical approaches; if you are already familiar with them then you are welcome to scroll down straight to ‘The Extensions Approach’ paragraph and learn how we resolve these challenges in what we think is a more efficient way.

    All in One

    The most straightforward and obvious solution to customization is to implement everything required in one core product and then employ the ‘feature toggle’ technique to match the requirements of each particular customer.
    The main advantage of the All in One approach is keeping the monolithic product, which appears to be a good way for certain types of products which generally cover business requirements without the need for extensive customization.

    The natural limitation of this approach is hidden in the ‘not much customization required’ assumption. Often, a product development starts with this belief, but after a number of deliveries you realize the true scale of how much customer-specific functionality is required. It’s not uncommon to be caught on the horns of a dilemma; refuse custom development and potentially lose customers, or turn the source code into a garbage can with single-customer-specific features which are likely to be useless for the majority of end-users.

    Which option would you choose? Obviously, choosing between a rock and a hard place is not the way to success.

    Summary: The All in One approach can be an appropriate choice only if you are sure that rare and limited customizations will be required. Otherwise, you’ll be faced by the choice between manageable and supportable product vs. customer satisfaction. Let me quote Jerry Garcia, who said: ‘Constantly choosing the lesser of two evils is still choosing evil’.

    Branching

    If significant customization is the ‘must have’ part of a delivery then the All in One technique cannot be employed. There is another straightforward approach - branching. You simply branch the product codebase and change whatever in isolation.

    Comparing Branching with All in One, the biggest advantage is that there are no limitations applicable to the scope of customization. You use separate branches to meet the specific requirements of different customers and avoid mixing all features in the same codebase.

    However, there is a flip side of it that can become a dead end in terms of product evolution. Obviously, the product branch is the main development space: Most of the bugfixes, improvements, new functionality first get pushed to the product. Thus, frequent merging is required to keep all the customized branches synchronised with the core product. Merge is a simple operation as long as the original product source code has not been affected by the customized branch, otherwise it becomes extremely time consuming and can lead to unavoidable regression bugs.

    This approach can still work if you are limited to a very few customized branches. However, as the number of delivered instances grows, the likelihood of facing the ‘torture by merge’ becomes imminent.

    Summary: The Branching approach is undoubtedly very flexible and straightforward – any part of a product can be modified. However, the post-delivery stage is potentially very laborious, becoming more difficult over time and it is very unlikely to result in delivering a significant number of manageable customized branches.

    The Entity-Attribute-Value Model

    The Entity-Attribute-Value Model (aka object–attribute–value model, vertical database model and open schema) is a well-known and widely-used data model. EAV enables dynamic entity attributes support and is typically used in parallel with the standard relational model.

    From productization point of view the main advantage of using EAV is that you can deliver a product ‘as is’ and then adjust the data model by adding required attributes at runtime, keeping the source code clean.

    As ever, there is a downside:

    • Limited applicability – The EAV model is limited by only enabling the addition of attributes to entities, which will then be auto-embedded to the UI, according to the pre-programmed logic.
    • Extra database server load - The vertical database design often becomes a bottleneck for enterprise applications, which generally operate with large numbers of entities and attributes related to them.

    Finally, enterprise systems cannot be imagined without the presence of a sophisticated reporting engine. The EAV model has the potential to bring in numerous complications because of its 'vertical' database structure.

    Summary: The Entity-Attribute-Value model has great value in certain situations, such as when there is a need to provide the flexibility achieved by having additional informative data, which is not explicitly used in business logic. In other words, EAV is good in moderation, e.g. in addition to the standard relational model and plugin architecture.

    The Plugin Architecture

    The Plugin architecture is one of the most popular and powerful approaches – where functional logic is kept as separate artefacts known as plugins. To override existing out-of-the-box behaviour and run plugins, it is necessary to define the 'Points of Customization' (aka Extension Points) in the product source code. A 'Point of Customization' is a certain spot in source code where an application leafs through the attached plugins to check if a plugin contains overridden implementation to be run here. One of the variations of the plugin architecture is external scripting; when functional implementation is implemented and stored externally as a script. Script invocation is also controlled by predefined 'customization points'.

    Using this plugin approach it is possible to keep the product 'clean' of specific customer requirements, deliver the core product ‘as is’ and customize the behaviour as required in plugins or scripts. Another advantage of this approach is a well managed update procedure. The full separation of product and plugin functionality enables each to be updated independently of each other.

    Of course, limitations exist: The principal limitation is that it is impossible to have complete knowledge of which custom requirements could be raised in the future. Consequently, it is only possible to guess where 'points of customization' should be embedded. Of course, these could be scattered everywhere as a mitigating ‘Just in case’ plan, but this will lead to poor code readability, hard debug and additionally complicated support.

    Summary: The Plugin architecture does work if the 'customization points' are easy to predict, but be mindful that customization in between 'customization points' is impossible.

    The Extensions Approach

    We have implemented a unique approach in our enterprise software development platform CUBA. As articulated in our previous article, CUBA is very practical, living organism, created through a process of developer-driven evolution. So, based on our vast experience of off-the-shelf products, we came up with two ultimate requirements:

    • Customer-specific code should be fully separated from the core product code.
    • Every part of the product code should be available for modifications.

    We managed to satisfy these requirements and achieve even more with our ‘Extensions’ mechanism.

    CUBA Extensions

    An extension is a separate CUBA project which inherits all the features of an underlying project (i.e. your core product), using it as a library. This obviously enables developers to implement completely new functionality without affecting the parent project, but thanks to the use of the Open Inheritance pattern and special CUBA facilities, you can also override any part of the parent project. In summary, an extension is the place where you implement the hundreds of ‘few minor details’ discussed at the beginning of this article.

    In fact, every CUBA project is an extension of the CUBA platform itself - so it can override any platform feature. We adopted this approach ourselves to split out some out-of-the-box features (Full Text Search, Reporting, Charting, etc.) from the core platform. So if you need them in your project, you just add them as parent projects - that's it, kind of multiple inheritance!

    In the same fashion, you can build a hierarchical customization model. This might sound complex, but it makes perfect sense. Let me give a real-world example: Sherlock - is Haulmont’s complete Taxi Management Solution, supporting every aspect of running a taxi business from booking and dispatch to apps and billing. The solution covers many different aspects of the client businesses and quite a few of them are location-related. E.g. all UK taxi companies have the same legal regulations, but many of them are not applicable for the United States, and vice versa. Obviously, we don’t want to implement all those regulations in the core product, because:

    • this is an ‘operation area specific’ feature
    • local regulations may have completely different effects for taxi fleet operations in different countries
    • some of the customers don’t require regulatory control at all

    So, we organize the multilevel extensions hierarchy:

    1. Core product contains the generic features of taxi business
    2. The first level of customization implements regional specifics
    3. The second level of customization covers the customer’s wish list (if there is one!)

    text

    Clean and clear.

    As you see, by using extensions you neither need branching nor integrating all requirements in the core product, the code remains clean and manageable. It sounds too good to be true, so let's see how it works in practice!

    Adding a New Attribute to an Existing Entity

    Let’s assume we have the product definition of User entity which consists of two fields: login and password:

    @Entity(name = "product$User")
    @Table(name = "PRODUCT_USER")
    public class User extends StandardEntity {
        @Column(name = "LOGIN")
        protected String login;
    
        @Column(name = "PASSWORD")
        protected String password;
    
        //getters and setters
    }
    

    Now we have got an additional requirement from some of our customers to add the ‘home address’ field to users. To do that we extend the User entity in the extension:

    @Entity(name = "ext$User")
    @Extends(User.class)
    public class ExtUser extends User {
    
        @Column(name = "ADDRESS", length = 100)
        private String address;
    
        public String getAddress() {
            return address;
        }
    
        public void setAddress(String address) {
            this.address = address;
        }
    }
    

    As you may have already noticed, all annotations, except for @Extends one, are common JPA annotations. The @Extends attribute is a part of the CUBA engine which globally substitutes the User entity to ExtUser, even across the product functionality.

    Using the @Extends attribute we force the platform to:

    1. always create an entity of the ‘latest child’ type

      User user = metadata.create(User.class); //ExtUser entity will be created
      
    2. transform all the JPQL queries before the execution so that they always return the ‘latest childset’

      select u from product$User u where u.name = :name //returns a list of ExtUsers
      
    3. always use the ‘latest child’ in the associated entities

      userSession.getUser(); //returns an instance of ExtUser type
      

    In other words, if an extended entity is declared, the base one is abandoned across the entire solution (product and extension) and is globally overridden by the extended one.

    Screens Customization

    So, we have extended the User entity by adding the address attribute and now want the changes to be reflected in the user interface. First, let us have a look on the original (product) screen declaration:

    <window
            datasource="userDs"
            caption="msg://caption"
            class="com.haulmont.cuba.gui.app.security.user.edit.UserEditor"
            messagesPack="com.haulmont.cuba.gui.app.security.user.edit">
    
        <dsContext>
            <datasource
                    id="userDs"
                    class="com.haulmont.cuba.security.entity.User"
                    view="user.edit">
               </datasource>
        </dsContext>
    
        <layout>
            <fieldGroup id="fieldGroup" datasource="userDs">
                <column>
                    <field id="login"/>
                    <field id="password"/>
                </column>
            </fieldGroup>
          
            <iframe id="windowActions" screen="editWindowActions"/>
        </layout>
    </window>
    

    As you can see, a CUBA screen descriptor is represented as an ordinary XML. Obviously, we could simply re-declare the entire screen descriptor in the extension, but this would mean copy-pasting most of it. In result, if something changes in the product screen in the future, we will have to copy these changes to the extension screen manually. To avoid this, CUBA introduces the screen inheritance mechanism and all you need is describe changes to the screen:

    <window extends="/com/haulmont/cuba/gui/app/security/user/edit/user-edit.xml">
        <layout>
            <fieldGroup id="fieldGroup">
                <column>
                    <field id="address"/>
                </column>
            </fieldGroup>
        </layout>
    </window>
    

    You define the ancestor screen using the extends attribute and only describe the subject to change.

    Here you are! Let’s finally have a look at the result:

    text

    Modification of the Business Logic

    To enable business logic modification the CUBA platform uses Spring Framework, which forms a core part of the platform infrastructure.

    For example, you have a middleware component to perform price calculation procedure:

    @ManagedBean("product_PriceCalculator")
    public class PriceCalculator {
        public void BigDecimal calculatePrice() { 
            //price calculation
        }
    }
    

    To override the price calculation implementation we only need to undertake two simple actions.

    First, extend the product class and override the corresponding procedure:

    public class ExtPriceCalculator extends PriceCalcuator {
        @Override
        public void BigDecimal calculatePrice() { 
                   //modified logic goes here
        }
    }
    

    And as a final touch, register the new class in the Spring configuration using the product bean identifier:

     <bean id="product_PriceCalculator" class="com.sample.extension.core.ExtPriceCalculator"/>
    

    Now PriceCalculator injection will always return the extended class instance. Consequently, the modified implementation will be used across the whole product.

    Upgrading Base Product Version in Extension

    As the core product evolves and new versions are released you will eventually decide to upgrade your extension to the latest product version. The process is quite simple:

    1. Specify the new version of the underlying product in extension.
    2. Rebuild the extension:
    • If an extension is built over the stable parts of the product API, then it is ready to run.
    • If some significant modifications of the product API have taken place and those modifications overlap customization implemented in the extension, it will be necessary to support the new product API in the extension.

    Most of the time a product API does not change significantly from update to update, especially in minor releases. But even if an API ‘big-bang’ occurs, a product usually keeps downward compatibility for at least a couple of future versions and the old implementation is marked as ‘deprecated’, allowing all extensions to be migrated to the newest API.

    Conclusion

    As a short summary I would like to illustrate the result of the comparative analysis in a tabular form:

    text

    As you can see the Extension approach is powerful, but one thing it lacks is the ability to fine-tune the system on the fly (dynamically customized). To overcome this, CUBA also provides full support for the Entity-Attribute-Value model and Plugin/Scripting approach.

    I hope you will find this overview useful and of course your feedback is much appreciated.

    Jmix is an open-source platform for building enterprise applications in Java