Shopware 6.7 delivers a highly modular, event-driven e-commerce architecture, but out-of-the-box recurring order handling remains plugin territory. Whether you're selling consumables, managing B2B replenishment contracts, or offering membership tiers, building custom subscription logic is often unavoidable. This guide walks you through architecting, implementing, and scaling recurring workflows in Shopware 6.7+, leveraging modern framework capabilities while avoiding common architectural anti-patterns.

Architectural Foundations

Before writing code, align your approach with Shopware’s event-driven philosophy. A subscription isn’t merely a recurring invoice; it’s a lifecycle-managed agreement tied to customer identity, payment method validity, fulfillment constraints, and sales channel isolation. Shopware 6.7+ explicitly discourages direct EntityManager manipulation or synchronous cron-style loops in controllers. Instead, the framework expects you to use:

  • Scheduled Tasks for time-bound execution
  • Doctrine Entity Relationships for data integrity
  • State Machine Handlers for predictable transitions
  • Event Subscribers for decoupled downstream reactions

Hardcoding renewal dates or bypassing Shopware’s queue system will quickly lead to race conditions, inconsistent order states, and scaling bottlenecks. Design your subscription engine as a set of small, testable services that communicate through repositories, domain events, and framework queues.

Core Components Breakdown

Custom Entity & Migration

Each subscription requires its own table to track metadata independent of orders: next_billing_date, status, payment_reference, cycle_interval, and linked_order_ids. Create the entity under src/Entity/ and define a migration in src/Migrations/Shopware67/. Use explicit foreign keys to customer, sales_channel, and order. Avoid embedding subscription logic inside swag_custom_fields or JSON blobs—Shopware 6.7’s ORM performs significantly better with normalized relations.

Scheduled Task Engine

Recurring processing must be asynchronous. Extend Shopware\Core\Framework\ScheduledTask\ScheduledTask and register it via the service container. In Shopware 6.7+, task registration relies on the shopware.scheduled_task tag and a static getTaskName() method. The framework’s queue worker will call your run() method at configured intervals. Inside run(), query entities where next_billing_date <= NOW(), validate constraints, trigger billing attempts, and schedule the next cycle. Never perform heavy I/O or block the main thread.

State Machine Integration

Use Shopware’s state machine API to manage subscription lifecycles (active, awaiting_payment, payment_failed, paused, cancelled, completed). Manual status updates break audit trails and prevent payment providers from hooking into transitions. Instead, persist new states via the StateMachineHandlerRegistry and dispatch domain events like SubscriptionBillingAttemptedEvent. This enables idempotent retry logic, webhook synchronization, and clean separation of concerns.

Implementation Patterns & Code Examples

Idempotency & Retry Strategy

Subscription processing must survive crashes, duplicate queue messages, and gateway timeouts. Enforce unique constraints on subscription IDs and payment attempts. Implement exponential backoff for failed transactions. Store intermediate processing states in Redis rather than relying on database row locks, which degrade under concurrent scheduling. Always log execution contexts with correlation IDs for traceability.

Payment Handler Abstraction

Avoid coupling your engine to a single gateway. Define a SubscriptionBillingHandlerInterface and register implementations via service tags. Your scheduled task should resolve handlers dynamically based on the subscription’s stored payment method ID. Delegate charge attempts, capture authorization headers, and map webhook responses back to state transitions asynchronously.

// src/Core/Subscription/ScheduledTask/SubscriptionBillingTask.php
declare(strict_types=1);

namespace Swag\Subscription\Core\Subscription\ScheduledTask;

use Shopware\Core\Framework\ScheduledTask\ScheduledTask;

class SubscriptionBillingTask extends ScheduledTask
{
    public static function getTaskName(): string
    {
        return 'subscription_billing';
    }

    public static function getDefaultInterval(): int
    {
        return 3600; // Hourly execution
    }
}
<!-- src/Resources/config/services.xml -->
<service id="Swag\Subscription\Core\Subscription\ScheduledTask\SubscriptionBillingTask">
    <tag name="shopware.scheduled-task" />
</service>

Event Subscriber for Post-Billing Actions

Create a subscriber that listens to SubscriptionBillingAttemptedEvent and triggers downstream processes: invoice generation, stock allocation, webhook dispatch, and customer notification. Use Shopware 6.7’s EventDispatcher and ensure your subscriber is marked with the appropriate event tag in services.xml. Keep subscribers stateless and delegate heavy work to queue workers.

Best Practices & Pitfalls to Avoid

  • Never bypass repository patterns. Use EntityRepository::upsert() for batch updates, but validate constraints first to avoid ORM exceptions.
  • Store timestamps in UTC. Convert only at the presentation layer using Shopware’s locale manager or storefront context.
  • Respect multi-store architecture. Subscription visibility must honor sales channel filters and customer group restrictions. Hardcoded cross-channel queries break isolation.
  • Monitor queue depth and execution time. Long-running subscription jobs stall the entire scheduled task engine. Cap batches at 500 entities per run and offload heavy I/O to background workers.
  • Test edge cases thoroughly. Simulate failed renewals, partial payments, mid-cycle cancellations, timezone shifts, and payment method expirations. Subscription logic fails quietly without comprehensive integration tests.

Conclusion

Building subscription logic in Shopware 6.7 requires respecting the platform’s architectural boundaries while extending them thoughtfully. By combining scheduled tasks, state machines, event subscribers, and payment abstractions, you create a resilient, maintainable system that scales with business growth. Start small, validate against Shopware’s coding standards, and always prioritize idempotency, observability, and queue-friendly design in recurring workflows. The platform’s strength lies in its extensibility; use it wisely, and your subscription engine will outlive framework versions without technical debt.