diff --git a/commerce_sofortbanking.services.yml b/commerce_sofortbanking.services.yml
new file mode 100644
index 0000000..d09c0ad
--- /dev/null
+++ b/commerce_sofortbanking.services.yml
@@ -0,0 +1,13 @@
+services:
+  commerce_sofortbanking.order_lock:
+    class: Drupal\commerce_sofortbanking\OrderLock
+    arguments:
+      - '@lock'
+      - '@logger.factory'
+
+  commerce_sofortbanking.order_lock_release_event_subscriber:
+    class: Drupal\commerce_sofortbanking\EventSubscriber\OrderLockReleaseEventSubscriber
+    arguments:
+      - '@commerce_sofortbanking.order_lock'
+    tags:
+      - { name: event_subscriber }
diff --git a/src/EventSubscriber/OrderLockReleaseEventSubscriber.php b/src/EventSubscriber/OrderLockReleaseEventSubscriber.php
new file mode 100644
index 0000000..349e82c
--- /dev/null
+++ b/src/EventSubscriber/OrderLockReleaseEventSubscriber.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Drupal\commerce_sofortbanking\EventSubscriber;
+
+use Drupal\commerce_sofortbanking\OrderLockInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\Event\PostResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * Event subscriber releasing active order locks on terminating the request.
+ */
+class OrderLockReleaseEventSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The order lock service.
+   *
+   * @var \Drupal\commerce_sofortbanking\OrderLockInterface
+   */
+  protected $orderLock;
+
+  /**
+   * Constructs a new OrderLockReleaseEventSubscriber object.
+   *
+   * @param \Drupal\commerce_sofortbanking\OrderLockInterface $order_lock
+   *   The order lock service.
+   */
+  public function __construct(OrderLockInterface $order_lock) {
+    $this->orderLock = $order_lock;
+  }
+
+  /**
+   * Releases open order locks once output flushed.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\PostResponseEvent $event
+   *   The post response event.
+   */
+  public function releaseOrderLocks(PostResponseEvent $event) {
+    $this->orderLock->releaseAll();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    return [
+      KernelEvents::TERMINATE => 'releaseOrderLocks',
+    ];
+  }
+
+}
diff --git a/src/OrderLock.php b/src/OrderLock.php
new file mode 100644
index 0000000..e9d57ea
--- /dev/null
+++ b/src/OrderLock.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Drupal\commerce_sofortbanking;
+
+use Drupal\commerce_order\Entity\OrderInterface;
+use Drupal\Core\Lock\LockBackendInterface;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+
+/**
+ * Default order lock service implementation.
+ */
+class OrderLock implements OrderLockInterface {
+
+  /**
+   * The locking layer instance.
+   *
+   * @var \Drupal\Core\Lock\LockBackendInterface
+   */
+  protected $lock;
+
+  /**
+   * The logger.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * Static cache of active order locks.
+   *
+   * @var string[]
+   */
+  protected $activeLocks;
+
+  /**
+   * Constructs a new OrderLock object.
+   *
+   * @param \Drupal\Core\Lock\LockBackendInterface $lock
+   *   The locking layer instance.
+   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_channel_factory
+   *   The logger channel factory.
+   */
+  public function __construct(LockBackendInterface $lock, LoggerChannelFactoryInterface $logger_channel_factory) {
+    $this->lock = $lock;
+    $this->logger = $logger_channel_factory->get('commerce_sofortbanking');
+    $this->activeLocks = [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function lock(OrderInterface $order, $max_lock_attempts = 1, $log_failed = FALSE) {
+    $lock_attempts = 0;
+    $lock_name = $this->getLockName($order);
+    while (!$this->lock->lockMayBeAvailable($lock_name)) {
+      $lock_attempts++;
+      if ($lock_attempts > $max_lock_attempts) {
+        return FALSE;
+      }
+      if ($log_failed) {
+        $this->logger->info('Order ID @order_id needs wait to process, as no lock is available currently (most likely processed in parallel).',  [
+          '@order_id' => $order->id(),
+        ]);
+      }
+      // Wait for webhooks (IPN) or customer return request running in parallel.
+      $this->lock->wait($lock_name, 5);
+    }
+    $success = $this->lock->acquire($lock_name, 5);
+    if ($success) {
+      $this->activeLocks[$lock_name] = $lock_name;
+    }
+    return $success;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function release(OrderInterface $order) {
+    $lock_name = $this->getLockName($order);
+    if (isset($this->activeLocks[$lock_name])) {
+      $this->lock->release($lock_name);
+      unset($this->activeLocks[$lock_name]);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function releaseAll() {
+    foreach ($this->activeLocks as $lock_name) {
+      $this->lock->release($lock_name);
+      unset($this->activeLocks[$lock_name]);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLockName(OrderInterface $order) {
+    return 'commerce_sofortbanking:' . $order->uuid();
+  }
+
+}
diff --git a/src/OrderLockInterface.php b/src/OrderLockInterface.php
new file mode 100644
index 0000000..5bd11e5
--- /dev/null
+++ b/src/OrderLockInterface.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Drupal\commerce_sofortbanking;
+
+use Drupal\commerce_order\Entity\OrderInterface;
+
+/**
+ * Defines the order lock service interface.
+ */
+interface OrderLockInterface {
+
+  /**
+   * Attempts to gather a lock for the given order.
+   *
+   * @param \Drupal\commerce_order\Entity\OrderInterface $order
+   *   The order entity to lock.
+   * @param int $max_lock_attempts
+   *   The maximum number of attempts to acquire the lock. Defaults to 1.
+   * @param bool $log_failed
+   *   Whether to log any lock attempt, where the lock is not available.
+   *   Defaults to FALSE.
+   *
+   * @return bool
+   *   TRUE, if a lock has been acquired. FALSE otherwise.
+   */
+  public function lock(OrderInterface $order, $max_lock_attempts = 1, $log_failed = FALSE);
+
+  /**
+   * Releases the lock for the given order.
+   *
+   * @param \Drupal\commerce_order\Entity\OrderInterface $order
+   *   The order entity.
+   */
+  public function release(OrderInterface $order);
+
+  /**
+   * Releases any open order lock of the current request.
+   */
+  public function releaseAll();
+
+  /**
+   * Returns the lock name for the given order.
+   *
+   * @param \Drupal\commerce_order\Entity\OrderInterface $order
+   *   The order.
+   *
+   * @return string
+   *   The lock name.
+   */
+  public function getLockName(OrderInterface $order);
+
+}
\ No newline at end of file
diff --git a/src/Plugin/Commerce/PaymentGateway/SofortGateway.php b/src/Plugin/Commerce/PaymentGateway/SofortGateway.php
index a85d854..c01a0c4 100644
--- a/src/Plugin/Commerce/PaymentGateway/SofortGateway.php
+++ b/src/Plugin/Commerce/PaymentGateway/SofortGateway.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\commerce_sofortbanking\Plugin\Commerce\PaymentGateway;
 
+use Drupal\commerce\Response\NeedsRedirectException;
 use Drupal\commerce_order\Entity\OrderInterface;
 use Drupal\commerce_payment\Entity\PaymentInterface;
 use Drupal\commerce_payment\Exception\InvalidRequestException;
@@ -11,11 +12,14 @@ use Drupal\commerce_payment\PaymentTypeManager;
 use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayBase;
 use Drupal\commerce_price\Price;
 use Drupal\commerce_price\RounderInterface;
+use Drupal\commerce_sofortbanking\OrderLockInterface;
 use Drupal\Component\Datetime\TimeInterface;
 use Drupal\Core\Datetime\DateFormatterInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Url;
 use Sofort\SofortLib\Notification;
 use Sofort\SofortLib\Refund;
 use Sofort\SofortLib\Sofortueberweisung;
@@ -76,6 +80,20 @@ class SofortGateway extends OffsitePaymentGatewayBase implements SofortGatewayIn
    */
   protected $rounder;
 
+  /**
+   * The order lock service.
+   *
+   * @var \Drupal\commerce_sofortbanking\OrderLockInterface
+   */
+  protected $orderLock;
+
+  /**
+   * The current route match service.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
   /**
    * Constructs a new SofortGateway object.
    *
@@ -99,10 +117,14 @@ class SofortGateway extends OffsitePaymentGatewayBase implements SofortGatewayIn
    *   The logger channel factory.
    * @param \Drupal\commerce_price\RounderInterface $rounder
    *   The price rounder.
+   * @param \Drupal\commerce_sofortbanking\OrderLockInterface $order_lock
+   *   The order lock service.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
    *
    * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, PaymentTypeManager $payment_type_manager, PaymentMethodTypeManager $payment_method_type_manager, TimeInterface $time, DateFormatterInterface $date_formatter, LoggerChannelFactoryInterface $logger_channel_factory, RounderInterface $rounder) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, PaymentTypeManager $payment_type_manager, PaymentMethodTypeManager $payment_method_type_manager, TimeInterface $time, DateFormatterInterface $date_formatter, LoggerChannelFactoryInterface $logger_channel_factory, RounderInterface $rounder, OrderLockInterface $order_lock, RouteMatchInterface $route_match) {
     parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $payment_type_manager, $payment_method_type_manager, $time);
 
     $this->dateFormatter = $date_formatter;
@@ -125,7 +147,9 @@ class SofortGateway extends OffsitePaymentGatewayBase implements SofortGatewayIn
       $container->get('datetime.time'),
       $container->get('date.formatter'),
       $container->get('logger.factory'),
-      $container->get('commerce_price.rounder')
+      $container->get('commerce_price.rounder'),
+      $container->get('commerce_sofortbanking.order_lock'),
+      $container->get('current_route_match')
     );
   }
 
@@ -249,7 +273,20 @@ class SofortGateway extends OffsitePaymentGatewayBase implements SofortGatewayIn
 
     $sofort_notification = new Notification();
     $transaction_id = $sofort_notification->getNotification($request->getContent());
-    $this->processNotification($transaction_id);
+    $payment = $this->paymentStorage->loadByRemoteId($transaction_id);
+    if (empty($payment)) {
+      throw new InvalidRequestException('No transaction found for SOFORT transaction ID @transaction_id.', ['@transaction_id' => $transaction_id]);
+    }
+    $gateway = $payment->getPaymentGateway()->getPlugin();
+    if (!($gateway instanceof SofortGatewayInterface)) {
+      throw new \InvalidArgumentException('Payment @payment_id is not an SOFORT payment.', ['@payment_id', $payment->id()]);
+    }
+    $has_lock = $this->orderLock->lock($payment->getOrder());
+    if (!$has_lock) {
+      throw new PaymentGatewayException('Cannot acquire lock for given order - try again later!');
+    }
+
+    $this->processNotification($payment);
     return NULL;
   }
 
@@ -286,7 +323,24 @@ class SofortGateway extends OffsitePaymentGatewayBase implements SofortGatewayIn
     }
 
     $transaction_id = $sofort_gateway_data['transaction_id'];
-    $this->processNotification($transaction_id);
+    $payment = $this->paymentStorage->loadByRemoteId($transaction_id);
+    if (empty($payment)) {
+      throw new InvalidRequestException('No transaction found for SOFORT transaction ID @transaction_id.', ['@transaction_id' => $transaction_id]);
+    }
+    $gateway = $payment->getPaymentGateway()->getPlugin();
+    if (!($gateway instanceof SofortGatewayInterface)) {
+      throw new \InvalidArgumentException('Payment @payment_id is not an SOFORT payment.', ['@payment_id', $payment->id()]);
+    }
+
+    $has_lock = $this->orderLock->lock($order, 5, TRUE);
+    if (!$has_lock) {
+      $this->logger->warning('Cannot acquire order lock for 5 times. Skip processing payment state. This has to be done via webhooks call - payment ID: @payment_id / order ID: @order_id', [
+        '@payment_id' => $payment->id(),
+        '@order_id' => $order->id(),
+      ]);
+      return;
+    }
+    $this->processNotification($payment);
   }
 
   /**
@@ -331,10 +385,7 @@ class SofortGateway extends OffsitePaymentGatewayBase implements SofortGatewayIn
   /**
    * Processes SOFORT response for both return url and async notification.
    *
-   * @param string $transaction_id
-   *   The SOFORT transaction ID to process.
-   *
-   * @return \Drupal\commerce_payment\Entity\PaymentInterface
+   * @param \Drupal\commerce_payment\Entity\PaymentInterface $payment
    *   The payment entity.
    *
    * @throws \Drupal\commerce_payment\Exception\InvalidRequestException
@@ -345,25 +396,43 @@ class SofortGateway extends OffsitePaymentGatewayBase implements SofortGatewayIn
    *   For the sake of completeness, as Drupal storage classes could
    *   theoretically throw this exception on load.
    */
-  protected function processNotification($transaction_id) {
-    $payment = $this->paymentStorage->loadByRemoteId($transaction_id);
-    if (empty($payment)) {
-      throw new InvalidRequestException('No transaction found for SOFORT transaction ID @transaction_id.', ['@transaction_id' => $transaction_id]);
-    }
-
+  protected function processNotification(PaymentInterface $payment) {
+    $order = $payment->getOrder();
+    $transaction_id = $payment->getRemoteId();
     $sofort_transaction = new TransactionData($this->configuration['configuration_key']);
     $sofort_transaction->addTransaction($transaction_id);
     $sofort_transaction->sendRequest();
     $sofort_transaction->setApiVersion('2.0');
     if ($sofort_transaction->isError()) {
+      $this->orderLock->release($order);
       throw new PaymentGatewayException('Transaction status not available for SOFORT transaction ID @transaction_id (error: @error).', ['@transaction_id' => $transaction_id, '@error' => $sofort_transaction->getError()]);
     }
 
+    // We could have an outdated order, just reload it and check the states.
+    // @todo Change this when #3043180 is fixed.
+    /* @see https://www.drupal.org/project/commerce/issues/3043180 */
+    $order_storage = $this->entityTypeManager->getStorage('commerce_order');
+    $updated_order = $order_storage->loadUnchanged($order->id());
+    $payment = $this->paymentStorage->loadUnchanged($payment->id());
+
     $remote_state = $sofort_transaction->getStatus();
     $remote_state = empty($remote_state) ? 'cancel' : $remote_state;
-    if ($remote_state == $payment->getRemoteState()) {
-      // Nothing to do, this payment receipt has already been captured.
-      return $payment;
+    // If we have different states is because the payment has been validated
+    // on the onNotify() callback and we need to force the redirection to the
+    // next step or it the order will be placed twice.
+    if ($remote_state == $payment->getRemoteState() || $payment->isCompleted() || $updated_order->getState()->getId() != $order->getState()->getId()) {
+      $this->orderLock->release($order);
+      // Get the current checkout step and calculate the next step.
+      $step_id = $this->routeMatch->getParameter('step');
+      /** @var \Drupal\commerce_checkout\Entity\CheckoutFlowInterface $checkout_flow */
+      $checkout_flow = $order->get('checkout_flow')->first()->get('entity')->getTarget()->getValue();
+      $checkout_flow_plugin = $checkout_flow->getPlugin();
+      $redirect_step_id = $checkout_flow_plugin->getNextStepId($step_id);
+
+      throw new NeedsRedirectException(Url::fromRoute('commerce_checkout.form', [
+        'commerce_order' => $updated_order->id(),
+        'step' => $redirect_step_id,
+      ])->toString());
     }
 
     $payment->setRemoteState($remote_state);
@@ -402,7 +471,6 @@ class SofortGateway extends OffsitePaymentGatewayBase implements SofortGatewayIn
         break;
     }
     $payment->save();
-    return $payment;
   }
 
   /**
