Best practices

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

Ask about this Page
Copy for LLM
View as Markdown

After completing this page, you should be able to:

  • Distinguish best practices related to maintaining resources in commercetools.

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.

Combine requests
import { apiRoot } from '../impl/apiClient.js';

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.

Error handling
import { apiRoot } from '../impl/apiClient.js';

const customerKey = 'abcdefghijklm';
const customerGroupId = 'silver';

async function fetchCustomer(customerKey) {
  try {
    const customerResponse = await apiRoot
      .customers()
      .withKey({ key: customerKey })
      .get()
      .execute();
    return customerResponse.body;
  } catch (error) {
    console.log(JSON.stringify(error, null, 2));
    throw error;
  }
}

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) {
    console.log(JSON.stringify(error, null, 2));
    throw error;
  }
}

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) {
    console.log(JSON.stringify(error, null, 2));
  }
}

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.

Logging
import { apiRoot } from '../impl/apiClient.js';

/* 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;
  }
}
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.

TypeScript

Java

C#


With the HttpMiddleware, you can add the retryConfig policy to retry transient 5xx responses as well as timed-out or aborted connections, using exponential backoff. For the full retryConfig options, see the HttpMiddleware reference.

To address HTTP 409 errors, which indicate concurrent modification conflicts, use the concurrent modification middleware. This middleware retries the request with the updated resource version when a 409 Conflict error occurs.

const concurrentModificationMiddlewareOptions = {
  concurrentModificationHandlerFn: (version, request) => {
    request.body.version = version;
    return JSON.stringify(request.body);
  },
};

Use withPolicies to add a retry policy. Use statusCodes to retry transient 5xx responses and failures to retry connection-level exceptions such as java.io.IOException.

Use addConcurrentModificationMiddleware to automatically retry the request with the correct version number upon 409 errors. Learn more about ConcurrentModificationMiddleware.

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.INTERNAL_SERVER_ERROR_500,
                    HttpStatusCode.BAD_GATEWAY_502,
                    HttpStatusCode.SERVICE_UNAVAILABLE_503,
                    HttpStatusCode.GATEWAY_TIMEOUT_504
                ))
                .failures(Arrays.asList(IOException.class))
        )
    )
    .build(projectKey);

The .NET SDK builds on IHttpClientFactory, so you add a retry policy with a Polly policy and attach it to the client with AddPolicyHandler. This will address 503 and 500 errors.

Unlike the Java SDK, the .NET SDK has no built-in concurrent modification middleware. To handle 409 errors, catch ConcurrentModificationException and retry the request with the CurrentVersion returned in the error.

// Include the following imports:
using System.Net;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Polly;
using Polly.Extensions.Http;
using commercetools.Sdk.Api;
using commercetools.Base.Client.Error;
using commercetools.Sdk.Api.Models.Errors;

var services = new ServiceCollection();

var configuration = new ConfigurationBuilder()
  .AddInMemoryCollection(new List<KeyValuePair<string, string>>()
  {
    new KeyValuePair<string, string>("Client:ApiBaseAddress", "https://api.{region}.commercetools.com/"),
    new KeyValuePair<string, string>("Client:AuthorizationBaseAddress", "https://auth.{region}.commercetools.com/"),
    new KeyValuePair<string, string>("Client:ClientId", "{clientID}"),
    new KeyValuePair<string, string>("Client:ClientSecret", "{clientSecret}"),
    new KeyValuePair<string, string>("Client:ProjectKey", "{projectKey}")
  })
  .Build();

// Create a retry policy that addresses 503 and 500 errors.
var retryPolicy = HttpPolicyExtensions
  .HandleTransientHttpError()
  .OrResult(response =>
      response.StatusCode == HttpStatusCode.ServiceUnavailable ||
      response.StatusCode == HttpStatusCode.InternalServerError)
  .RetryAsync(3);

// Register the commercetools API client and attach the retry policy to its HTTP client.
services
  .UseCommercetoolsApi(configuration, "Client")
  .AddPolicyHandler(retryPolicy);

var serviceProvider = services.BuildServiceProvider();
var projectApiRoot = serviceProvider.GetService<commercetools.Sdk.Api.Client.ProjectApiRoot>();

// Unlike the Java SDK, the .NET SDK has no built-in concurrent modification middleware.
// Handle 409 errors by catching ConcurrentModificationException and retrying the request
// with the version returned in the error.
try
{
  // Send your update request here, for example a Customer update.
}
catch (ConcurrentModificationException error)
{
  var errorResponse = error.AsErrorResponse();
  var concurrentError = errorResponse.Errors.FirstOrDefault() as ConcurrentModificationError;
  long? currentVersion = concurrentError?.CurrentVersion;

  // Retry the update using currentVersion.
}

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

Test your knowledge