“It works on my local machine!” Nowadays it sounds like a meme, but the problem “development environment vs production environment” still exists. As a developer, you should always keep in mind that your application will start working in the production environment one day. In this article, we will talk about some CUBA-specific things that will help you to avoid problems when your application will go to production.
Coding Guidelines
Prefer Services
Almost every CUBA application implements some business logic algorithms. The best practice here is to implement all business logic in CUBA Services. All other classes: screen controllers, application listeners, etc. should delegate business logic execution to services. This approach has the following advantages:
- There will be only one implementation of the business logic in one place
- You can call this business logic from different places and expose it as a REST service.
Please remember that business logic includes conditions, loops, etc. It means that service invocations ideally should be a one-liner. For example, let’s assume that we have the following code in a screen controller:
Item item = itemService.findItem(itemDate);
if (item.isOld()) {
itemService.doPlanA(item);
} else {
itemService.doPlanB(item);
}
If you see code like this, consider moving it from the screen controller to the itemService
as a separate method processOldItem(Date date)
because it looks like a part of your application’s business logic.
Since screens and APIs can be developed by different teams, keeping business logic in one place will help you to avoid application behavior inconsistency in production.
Be Stateless
When developing a web application, remember that it will be used by multiple users. In the code, it means that some code can be executed by multiple threads at the same time. Almost all application components: services, beans as well as event listeners are affected by multithreading execution. The best practice here is to keep your components stateless. It means that you should not introduce shared mutable class members. Use local variables and keep the session-specific information in the application store that is not shared between users. For example, you can keep a small amount of serializable data in the user session.
If you need to share some data, use the database or a dedicated shared in-memory storage like Redis.
Use Logging
Sometimes something goes wrong in production. And when it happens, it is quite hard to figure out what exactly caused the failure, you cannot debug the application deployed to prod. To simplify further work for yourself, your fellow developers and support team and to help understand the issue and be able to reproduce it, always add logging to the application.
In addition, logging plays the passive monitoring role. After application restart, update or reconfiguration an administrator usually looks at logs to make sure that everything has started successfully.
And logging may help with fixing issues that may happen not in your application, but in the services that your application is integrated with. For instance, to figure out why a payment gateway rejects some transactions, you may need to record all the data and then use it during your talks with the support team.
CUBA uses a proven package of the slf4j library as a facade and logback implementation. You just need to inject the logging facility to your class code and you’re good to go.
@Inject
private Logger log;
Then just invoke this service in your code:
log.info("Transaction for the customer {} has succeeded at {}", customer, transaction.getDate());
Please remember that log messages should be meaningful and contain enough information to understand what has happened in the application. You can find a lot more logging tips for Java applications in the article series “Clean code, clean logs”. Also, we’d recommend having a look at the “9 Logging Sins” article.
Also, in CUBA we have performance statistics logs, so you can always see how the application consumes a server’s resources. It will be very helpful when customer’s support starts receiving users’ complaints about the application being slow. With this log in hands, you can find the bottleneck faster.
Handle Exceptions
Exceptions are very important because they provide valuable information when something goes wrong in your application. Therefore, rule number one - never ignore exceptions. Use log.error()
method, create a meaningful message, add context and stack trace. This message will be the only information that you will use to identify what happened.
If you have a code convention, add the error handling rules section into it.
Let’s consider an example - uploading a user’s profile picture to the application. This profile picture will be saved to the CUBA’s file storage and file upload API service.
This is how you must not deal with an exception:
try {
fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd);
} catch (Exception e) {}
If an error occurs, nobody will know about it and users will be surprised when they don’t see their profile picture.
This is a bit better, but far from ideal.
try {
fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd);
} catch (FileStorageException e) {
log.error (e.getMessage)
}
There will be an error message in logs and we will catch only particular exception classes. But there will be no information about context: what was the file’s name, who tried to upload it. Moreover, there will be no stack trace, so it will be quite hard to find where the exception occurred. And one more thing - a user won’t be notified about the issue.
This might be a good approach.
try {
fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd);
} catch (FileStorageException e) {
throw new RuntimeException("Error saving file to FileStorage", e);
}
We know the error, do not lose the original exception, add a meaningful message. The calling method will be notified about the exception. We could add the current user name and, probably, the file name to the message to add a bit more context data. This is an example of the CUBA web module.
In CUBA applications, due to their distributed nature, you might have different exception handling rules for core and web modules. There is a special section in the documentation regarding exception handling. Please read it before implementing the policy.
Environment Specific Configuration
When developing an application, try to isolate environment-specific parts of the application’s code and then use feature toggling and profiles to switch those parts depending on the environment.
Use Appropriate Service Implementations
Any service in CUBA consists of two parts: an interface (service API) and its implementation. Sometimes, the implementation may depend on the deploy environment. As an example, we will use the file storage service.
In CUBA, you can use a file storage to save files that have been sent to the application, and then use them in your services. The default implementation uses the local file system on the server to keep files.
But when you deploy the application to the production server, this implementation may not work well for cloud environments or for the clustered deployment configuration.
To enable environment-specific service implementations, CUBA supports runtime profiles that allow you to use a specific service depending on the startup parameter or the environment variable.
For this case, if we decide to use Amazon S3 implementation of the file storage in production, you can specify the bean in the following way:
<beans profile="prod">
<bean name="cuba_FileStorage" class="com.haulmont.addon.cubaaws.s3.AmazonS3FileStorage"/>
</beans>
And S3 implementation will be automatically enabled when you set the property:
spring.profiles.active=prod
So, when you develop a CUBA application, try to identify environment-specific services and enable proper implementation for each environment. Try not to write code that looks like this:
If (“prod”.equals(getEnvironment())) {
executeMethodA();
} else {
executeMethodB();
}
Try to implement a separate service myService
that has one method executeMethod()
and two implementations, then configure it using profiles. After that your code will look like this:
myService.executeMethod();
Which is cleaner, simpler and easier to maintain.
Externalize Settings
If possible, extract application settings to properties files. If a parameter can change in the future (even if the probability is low), always externalize it. Avoid storing connection URLs, hostnames, etc. as plain strings in the application’s code and never copy-paste them. The cost of changing a hardcoded value in the code is much higher. Mail server address, user’s photo thumbnail size, number of retry attempts if there is no network connection - all of these are examples of properties that you need to externalize. Use configuration interfaces and inject them into your classes to fetch configuration values.
Utilize runtime profiles to keep environment-specific properties in separate files.
For example, you use a payment gateway in your application. Of course, you should not use real money for testing the functionality during development. Therefore, you have a gateway stub for your local environment, test API on the gateway side for the pre-production test environment and a real gateway for the prod. And gateway addresses are different for these environments, obviously.
Do not write your code like this:
If (“prod”.equals(getEnvironment())) {
gatewayHost = “gateway.payments.com”;
} else if (“test”.equals(getEnvironment())) {
gatewayHost = “testgw.payments.com”;
} else {
gatewayHost = “localhost”;
}
connectToPaymentsGateway(gatewayHost);
Instead, define three properties files: dev-app.properties
, test-app.properties
and prod-app.properties
and define three different values for the database.host.name
property in these.
After that, define a configuration interface:
@Source(type = SourceType.DATABASE)
public interface PaymentGwConfig extends Config {
@Property(&quot;payment.gateway.host.name&quot;)
String getPaymentGwHost();
}
Then inject the interface and use it in your code:
@Inject
PaymentGwConfig gwConfig;
//service code
connectToPaymentsGateway(gwConfig.getPaymentGwHost());