# Best practices On this page we are going to look at four important best practices to develop maintainable, readable, and performant code: - We will combine requests without blocking code execution. - We will add error handling. - We will add extensive and meaningful logging. - We will add retry and concurrent modification handling. ## Combine requests Thus far we have used the SDKs to execute single request. Most of the time, we also provided the needed parameters written directly in the code to allow us to concentrate on a single operation. However, this is an anti-pattern for realistic development. Usually you would have your inputs written into variables either from configuration files, databases, or previous requests. Let’s take a look at the latter of these options. A typical example could be the assignment of a Customer to a Customer Group. Here, you would in a realistic scenario perform three requests: 1. You fetch the Customer. 2. You fetch the Customer Group. 3. Then you assign the Customer to the Customer Group. Translating this into our CRUD requests we would have to perform 1. A GET request to the Customers endpoint. 2. A GET request to the Customer Groups endpoint. 3. A POST request to the Customers endpoint. An anti-pattern would be to always block your code execution waiting for every single request to return the result before continuing. We strongly recommend not to do so. Use asynchronicity whenever you can! Let’s have a look at a potential implementation. As you can see, all hardcoded values are gone. ```typescript const customerKey = 'thomas-tools'; const customerGroupId = 'gold'; async function fetchCustomer(customerKey) { const customerResponse = await apiRoot .customers() .withKey({ key: customerKey }) .get() .execute(); return customerResponse.body; } async function updateCustomerGroup( customerKey, customerGroupId, customerVersion ) { const updateResponse = await apiRoot .customers() .withKey({ key: customerKey }) .post({ body: { version: customerVersion, actions: [ { action: 'setCustomerGroup', customerGroup: { key: customerGroupId }, }, ], }, }) .execute(); return updateResponse.body; } async function customerUpdateGroupCombine(customerKey, customerGroupId) { const customer = await fetchCustomer(customerKey); const customerUpdate = await updateCustomerGroup( customerKey, customerGroupId, customer.version ); console.log( 'Customer group updated successfully:', JSON.stringify(customerUpdate, null, 2) ); } customerUpdateGroupCombine(customerKey, customerGroupId); ``` ```java CustomerService: public CompletableFuture> assignCustomerToCustomerGroup( final ApiHttpResponse customerApiHttpResponse, final ApiHttpResponse customerGroupApiHttpResponse) { // Step 1: Unpack the objects from the previous requests final Customer customer = customerApiHttpResponse.getBody(); final CustomerGroup customerGroup = customerGroupApiHttpResponse.getBody(); // Step 2: Add some logging // Step 3: Prepare the next request return apiRoot .customers() .withKey(customer.getKey()) .post(CustomerUpdateBuilder.of() .version(customer.getVersion()) .actions( CustomerSetCustomerGroupActionBuilder.of() .customerGroup(CustomerGroupResourceIdentifierBuilder.of() .key(customerGroup.getKey()) .build()) .build() ) .build()) .execute(); } CustomerExercise: logger.info( "Updated customer with the customer group " + client .customers() .withKey(customerKey) .get() .execute() .thenComposeAsync(customerApiHttpResponse -> client.customers() .withKey(customerKey) .post( CustomerUpdateBuilder.of() .version(customerApiHttpResponse.getBody().getVersion()) .actions( CustomerSetCustomerGroupActionBuilder.of() .customerGroup( CustomerGroupResourceIdentifierBuilder.of() .key(customerGroupKey).build()) .build()) .build() ) .execute() ).get().getBody().getId() ``` ## Error handling Add error handling to your code to make it robust and to be able to recover from such errors. At a minimum, you might want to learn from the errors to improve your code. A special note on Java: You might want to use optional classes if you prefer to reduce the code directly used in the request. ```typescript assignCustomerToCustomerGroup(customerDraft.key!, "vip-customers") .then(log) .catch( // Handle the error, create objects that the following computation can handle ); import { apiRoot } from "../impl/apiClient.js"; // Update to map to your API root const customerKey = "abcdefghijklm"; const customerGroupId = "silver"; const customerVersion = 1; async function customerUpdateGroup(customerKey, customerGroupId, version) { // Wrap code in a Try/ Catch /** * Fetches a customer by their key. * @param {string} customerKey - The key of the customer to fetch. * @returns {Promise} A Promise that resolves with the customer object. * @throws {Error} If the customer cannot be fetched. */ async function fetchCustomer(customerKey) { try { const customerResponse = await apiRoot .customers() .withKey({ key: customerKey }) .get() .execute(); return customerResponse.body; } catch (error) { // Log the error for debugging purposes console.log(JSON.stringify(error, null, 2)); // Re-throw the error for handling at a higher level throw error; } } /** * Updates a customer's group. * @param {string} customerKey - The key of the customer to update. * @param {string} customerGroupId - The ID of the customer group to assign. * @param {number} customerVersion - The version of the customer being updated. * @returns {Promise} A Promise that resolves with the updated customer object. * @throws {Error} If the customer group cannot be updated. */ async function updateCustomerGroup(customerKey, customerGroupId, customerVersion) { try { const updateResponse = await apiRoot .customers() .withKey({ key: customerKey }) .post({ body: { version: customerVersion, actions: [ { action: "setCustomerGroup", customerGroup: { key: customerGroupId }, }, ], }, }) .execute(); return updateResponse.body; } catch (error) { // Log the error for debugging purposes console.log(JSON.stringify(error, null, 2)); // Re-throw the error for handling at a higher level throw error; } } /** * Fetches a customer and updates their customer group. * @param {string} customerKey - The key of the customer to update. * @param {string} customerGroupId - The ID of the customer group to assign. * @throws {Error} If the customer cannot be fetched or the group cannot be updated. * This error could occur if the customer key or group ID is invalid, * or if there are issues connecting to the commercetools API. */ async function customerUpdateGroupCombine(customerKey, customerGroupId) { try { const customer = await fetchCustomer(customerKey); const customerUpdate = await updateCustomerGroup(customerKey, customerGroupId, customer.version); console.log("Customer group updated successfully:", JSON.stringify(customerUpdate, null, 2)); } catch (error) { // Log the error for debugging purposes console.log(JSON.stringify(error, null, 2)); // Consider more robust error handling in a production environment, // such as logging to an error tracking service or displaying a user-friendly message. } } customerUpdateGroupCombine(customerKey, customerGroupId); ``` ```java CustomerExercise:logger.info( "Customer assigned to group: " + customerService // truncated .exceptionally(throwable -> { // Do extensive logging logger.info(throwable.getLocalizedMessage()); // Return an object that can be handled by the following requests return null; }) .toCompletableFuture() .get() .getBody() .getEmail() ); ``` ## Logging Add extensive and very meaningful logging to your code. Remember that writing code never ends! Prepare for future code maintenance and adaptation. This brings us back to the Service class. If you transfer all requests into such service classes you can adapt your strategic and general logging strategy. ```typescript /* Configure the LoggerMiddleware inside your API client to log * requests and responses. For the full list of configuration options, see: * https://docs.commercetools.com/sdk/ts-sdk-middleware#loggermiddleware */ const customLoggerMiddleware = { logLevel: 'debug', httpMethods: ['POST', 'GET'], maskSensitiveData: true, logger: (method, ...args) => { console.log(`[CUSTOM LOGGER] ${method}`, ...args); }, }; async function fetchCustomer(customerKey) { try { const customerResponse = await apiRoot .customers() .withKey({ key: customerKey }) .get() .execute(); return customerResponse.body; } catch (error) { // The error will be logged by the commercetools SDK logging middleware // Re-throw the error for handling at a higher level throw error; } } ``` ```java CustomerService: public CompletableFuture> assignCustomerToCustomerGroup() //... // Step 2: Add some logging logger.info(customerApiHttpResponse.getBody()...); logger.info(customerApiHttpResponse.getHeader()...); // add more //... ``` You might want to always log the value `x-correlation-id` that is present in each response header. This value represents the unique id of the response from the commercetools API. Whenever you need to contact commercetools support and/or need to trace your request, this id is needed. Make sure you have it at hand when needed. ## Retry and concurrent modification handling When sending requests to the commercetools API you may have to handle a situation where you receive an error. commercetools might be shedding load at a certain moment of time, or the request you've sent may be incorrect. Let's look at ways to properly handle common errors like: - `500` server unavailability (`500` and above). - `409` version conflicts Let's see how we would handle these errors in the API middleware for both SDKs. With the [HttpMiddleware](https://docs.commercetools.com/learning-composable-commerce-developer-essentials/dev-tooling/ts-sdk-middleware.md#httpmiddleware), you can add the `retryConfig` policy and specify a set of error codes, retries, and delays. This will address `503` and `500` errors. For the full `retryConfig` options, see the [HttpMiddleware reference](https://docs.commercetools.com/learning-composable-commerce-developer-essentials/dev-tooling/ts-sdk-middleware.md#httpmiddleware). To address HTTP 409 errors, which indicate concurrent modification conflicts, use the [concurrent modification middleware](https://docs.commercetools.com/learning-composable-commerce-developer-essentials/dev-tooling/ts-sdk-middleware.md#concurrent-modification-middleware). This middleware retries the request with the updated resource version when a `409 Conflict` error occurs. ```typescript const concurrentModificationMiddlewareOptions = { concurrentModificationHandlerFn: (version, request) => { request.body.version = version; return JSON.stringify(request.body); }, }; ``` Use `withPolicies` to add a retry policy with a specified array of error codes and retries. This will address `503` and `500` errors. Use `addConcurrentModificationMiddleware` to automatically retry the request with the correct version number upon `409` errors. [Learn more about `ConcurrentModificationMiddleware`](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/com/commercetools/api/client/ConcurrentModificationMiddleware.html). ```java ProjectApiRoot apiRoot = ApiRootBuilder.of() .defaultClient( ClientCredentials.of() .withClientId(clientId) .withClientSecret(clientSecret) .build(), ServiceRegion.GCP_AUSTRALIA_SOUTHEAST1.getOAuthTokenUrl(), ServiceRegion.GCP_AUSTRALIA_SOUTHEAST1.getApiUrl() ) .addConcurrentModificationMiddleware(2, 200, 1000) .withPolicies(policyBuilder -> policyBuilder.withRetry(retry -> retry .maxRetries(3) .statusCodes(Arrays.asList( HttpStatusCode.SERVICE_UNAVAILABLE_503, HttpStatusCode.INTERNAL_SERVER_ERROR_500 )) ) ) .build(projectKey); ``` Now you are equipped with the basic operations to maintain your resources in a commercetools Project. ## Related pages - [Section overview page](https://docs.commercetools.com/learning-composable-commerce-developer-essentials.md) - [Previous page: Delete a Customer](https://docs.commercetools.com/learning-composable-commerce-developer-essentials/manage-resources-with-the-sdk/delete-a-customer.md) - [Next page: Learning check](https://docs.commercetools.com/learning-composable-commerce-developer-essentials/manage-resources-with-the-sdk/learning-check.md)