Best practices

Learn how to implement resource maintenance best practices when working with commercetools SDKs.

  • After completing this page, you should be able to:

    • Distinguish best practices related to maintaining resources in Composable Commerce.
  • 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 retry handling.
    • We will add more extensive and meaningful logging.

    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.

    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);

    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.

    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<object>} 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<object>} 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);

    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.

    /* Configure the Logger Middleware inside your API client and integrate it with your desired logging solution */
    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;
    }
    }

    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 Composable Commerce 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

    Lets see how we would handle these errors in the API middleware for both SDKs.

    TypeScript SDK

    With the HttpMiddleware, you can add the retryConfig policy and specify a set of error codes, retries, and delays. This will address 503 and 500 errors.

    To address HTTP 409 errors, which indicate concurrent modification conflicts, we can employ the withConcurrentModificationMiddleware. This middleware allows us to define a custom retry strategy.

    // Concurrent Modification Middleware Options
    const concurrentModificationMiddlewareOptions = {
    concurrentModificationHandlerFn: (version, request) => {
    console.log(`Concurrent modification error, retry with version ${version}`);
    request.body.version = version;
    return JSON.stringify(request.body);
    },
    };
    // Retries the request once with the updated version if a 409 (Conflict) error occurs.

    Java SDK

    Within the highlighted code, we added the retry policy, where we specified an array of error codes and retries. This will address 503 and 500 errors.

    Also included is the addConcurrentModificationMiddleware, which will automatically retry the request with the correct version number. Using a maxRetries of 2, delay 200 ms and maxDelay 1000 ms. This will address 409 errors. Learn more about ConcurrentModificationMiddleware.

    ClientService:
    public static ProjectApiRoot createApiClient() throws IOException {
    Properties props = new Properties();
    props.load(ClientService.class.getResourceAsStream("/dev.properties"));
    String clientId = props.getProperty("CTP_CLIENT_ID");
    String clientSecret = props.getProperty("CTP_CLIENT_SECRET");
    String projectKey = props.getProperty("CTP_PROJECT_KEY");
    projectApiRoot = 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);
    return projectApiRoot;
    }

    Now you are equipped with the basic operations to maintain your resources in a Composable Commerce Project.

    Test your knowledge