HCL Commerce ts-app customization bad practices - part 2 (performance)
- Jędrzej Małkiewicz
- May 7, 2024
- 6 min read
Updated: Oct 23
This is the second post from the series of posts where I highlight some of the mistakes that I saw throughout my journey with HCL Commerce that result in sub optimal implementations. In the first post from the series I described mistakes that arise from not applying HCL Commerce customization framework correctly. This article will concentrate on errors that negatively affect performance of the ts-app server.
This article will concentrate solely on HCL Commerce development aspects, excluding general Java coding best practices.
I hope we all agree that the quicker a response is served to clients the better and we as customers hate slow web shops. HCL Commerce is a great choice when it comes meeting that need (have a 'fast web shop') however there are certain simple rules that a development team needs to keep in mind when creating customization in order to make sure that response times are optimal.
This post will not cover the well-known practices such as 'running performance tests', 'tune the application' or 'enabling cache.' Instead, it will highlight less obvious pitfalls to avoid as early as possible in the development cycle.
Not understanding access profiles
An access profile determines granulity of data to be returned by a service. It's a parameter you pass to a GET service call that has a value like IBM_Summary, IBM_Details. Depending of the value the response may include more or less information. Of course the more data a service returns the more data it has to read to populate the response and the more data it has to read the slower it is. It's simple as that.
Now imagine you have to customize a cart response to just count the number of items in the cart that you want to display in the header of your UI. Nothing else.
In that case it's enough to just run an SQL query that will count number of order items that belong to a cart. That means it make sense to create a new access profile called EXT_counter (or whatever the naming convention you have in your project) instead using any of the out of the box ones that may carry order items details in the response.
One other thing related to access profiles is to make sure your API consumers understand their meaning, so that UI developers (or other type of callers of your API) use access profiles that gives them only data they need to implement a certain use-case.
Forgetting to add db indexes
Let's imagine that you had to extend the USERS table to contain some extra fields and you have created a new table called X_USERS with two columns called property1 and property2. Now imagine you need to offer a possibility to search by property1 or build some reports based on the value of property1. It might be a worth checking with your DBA if it might be a good idea to create an index to support the queries, especially if you have very little rows in your development environment's DB and possibly millions of them in a production DB. Lack of DB indexes can harm your production environment and that kind of defect can be easily 'smuggled' to production.
Skipping pagination
It's also very easy to do some harm if you forget about pagination. If you are asked to create a new API endpoint that returns reviews of a certain product but forget about adding support for pagination. Meaning you do not specify the default page size and allow consumers of the API to get data in chunks (pages of a certain size).
Your UI team may not inform you about that cause they 'have will pagination in the UI and will not observe any issues during development cause work with mocks'. Initially the site works perfectly fine but at some point you have some many comments and start getting complains.
If you do not add pagination to APIs that return collection of items you should definitely start doing that.
Finding a bean the wrong way
Imagine that inside of an extension of OrderPrepareCmdImpl you have to add some logic that depends on the total value of an order. In order to do that you add that piece of code to the overridden performExecute method.
super.performExecute();
OrderAccessBean order = (OrderAccessBean) new OrderAccessBean().findByOrderIds(new Long[]{Long.parseLong(this.getOrderRnParms()[0])}).nextElement();
... // do something with the read orderIt'll trigger 2 SQL statements
UPDATE ORDERS SET LASTUPDATE = ?, LOCKED = ?, OPTCOUNTER = ? WHERE ((ORDERS_ID = ?) AND (OPTCOUNTER = ?))
SELECT T1.ORDERS_ID, T1.ORMORDER, T1.ORGENTITY_ID, T1.TOTALPRODUCT, T1.TOTALTAX, T1.TOTALSHIPPING, T1.TOTALTAXSHIPPING, T1.DESCRIPTION, T1.STOREENT_ID, T1.CURRENCY, T1.LOCKED, T1.TIMEPLACED, T1.LASTUPDATE, T1.SEQUENCE, T1.STATUS, T1.MEMBER_ID, T1.FIELD1, T1.ADDRESS_ID, T1.FIELD2, T1.PROVIDERORDERNUM, T1.SHIPASCOMPLETE, T1.FIELD3, T1.TOTALADJUSTMENT, T1.ORDCHNLTYP_ID, T1.COMMENTS, T1.NOTIFICATIONID, T1.TYPE, T1.EDITOR_ID, T1.OPTCOUNTER, T1.SOURCEID, T1.EXPIREDATE, T1.BUSCHN_ID, T1.BLOCKED, T1.TRANSFERSTATUS, T1.OPSYSTEM_ID, T1.BUYERPO_ID FROM ORDERS T1 WHERE T1.ORDERS_ID IN (?)In contrary to:
super.performExecute();
OrderAccessBean order = new OrderAccessBean();
order.setInitKey_orderId(this.getOrderRnParms()[0]);
... // do something with the read orderThat will use a cached order object read by the OrderPrepareCmdImpl earlier and not execute any extra statement.
Let's consider another example where you want in the same command read all items from an order.
super.performExecute();
Enumeration<OrderItemAccessBean> orderItems = new OrderItemAccessBean().findByOrder(Long.parseLong(this.getOrderRnParms()[0]));
... // do something with the read itemsThis is code that we want to avoid as it'll result in at least 2 SQL statements:
SELECT T1.ORDERITEMS_ID, T1.STOREENT_ID, T1.TERMCOND_ID, T1.TRADING_ID, T1.ORDRELEASENUM, T1.ITEMSPC_ID, T1.CATENTRY_ID, T1.PARTNUM, T1.SHIPMODE_ID, T1.FFMCENTER_ID, T1.MEMBER_ID, T1.ADDRESS_ID, T1.ALLOCADDRESS_ID, T1.PRICE, T1.LINEITEMTYPE, T1.STATUS, T1.OUTPUTQ_ID, T1.INVENTORYSTATUS, T1.LASTCREATE, T1.LASTUPDATE, T1.FULFILLMENTSTATUS, T1.LASTALLOCUPDATE, T1.OFFER_ID, T1.TIMERELEASED, T1.TIMESHIPPED, T1.CURRENCY, T1.COMMENTS, T1.TOTALPRODUCT, T1.QUANTITY, T1.TAXAMOUNT, T1.TOTALADJUSTMENT, T1.SHIPTAXAMOUNT, T1.ESTAVAILTIME, T1.FIELD1, T1.DESCRIPTION, T1.FIELD2, T1.ALLOCATIONGROUP, T1.SHIPCHARGE, T1.BASEPRICE, T1.BASECURRENCY, T1.TRACKNUMBER, T1.TRACKDATE, T1.PREPAREFLAGS, T1.CORRELATIONGROUP, T1.ORDERS_ID, T1.PROMISEDAVAILTIME, T1.SHIPPINGOFFSET, T1.NEEDEDQUANTITY, T1.ALLOCQUANTITY, T1.ALLOCFFMC_ID, T1.CONFIGURATIONID, T1.SUPPLIERDATA, T1.SUPPLIERPARTNUMBER, T1.AVAILQUANTITY, T1.ISEXPEDITED, T1.REQUESTEDSHIPDATE, T1.TIECODE, T1.OPTCOUNTER FROM ORDERITEMS T1 WHERE (T1.ORDERS_ID = ?) AND (T1.STATUS <> 'X')
UPDATE ORDERS SET LASTUPDATE = ?, LOCKED = ?, OPTCOUNTER = ? WHERE ((ORDERS_ID = ?) AND (OPTCOUNTER = ?))A much better alternative to avoid extra SQL statements is to fetch order items from the order:
OrderAccessBean order = new OrderAccessBean();
order.setInitKey_orderId(this.getOrderRnParms()[0]);
OrderItemAccessBean [] orderItems = order.getOrderItems();
... // do something with the read itemsYou can clearly see now that not paying attention to how an entity bean is fetched can lead to extra SQL statements that may potentially slow down your site and you should aim to eliminate the ones that do not take advantage of data or JPA caches. Unfortunately it's hard to tell which finders will generate extra SQL statements so the only suggestion I can give you here is to profile the code or trace SQL statements.
Not caching custom entities
I assume that you know the benefits of caching frequently accessed data in memory. HCL Commerce has built-in data caches that sit on top of the most frequently used entities. The caching framework allows to cache custom entities, however that part is often neglected as it's requires extra setup and code. Does it mean you have to now add that for each custom object? No. Only to the ones that are either expensive to read (for example service calls) and do not change often or are likely to generate high cache hit ratios. When adding consider if it's worth caching or not.
Forgetting to set time-outs for web service calls
Regardless if you call a SOAP or REST endpoint remember to configure a connection and read timeout. Client libraries that are used usually have very long timeouts values set - very often 30 seconds or more. In a web facing environment you want your connections to be quick or fail fast. Last thing your users expect is to wait for 30 seconds in order to get a connection timeout exception, so read the documentation of a client library that you use and learn how to control the timeouts. Another thing you may consider in addition to controlling timeouts is applying a circuit breaker patter and be prepared to handle occasional failures.
Summary
In this post, I have presented some bad implementation habits that may slow down your shop. There are of course many more principles that if not followed will result in a bad performing site. However if you apply at least the above your site reliability engineers will be thankful :-)
In the final post from the series I'll focus on bad practices related to security of HCL Commerce customization.
Should you believe that your team could benefit from a code audit or customized technical enablement, please do not hesitate to contact us.
