Shopware 6.4+ App System vs Symfony Plugins: A Decision Guide for Extension Architecture
A decision guide comparing Shopware 6.4+ Apps (decoupled API-first services) vs traditional Symfony Plugins (in-process bundles). Covers constraints, trade-offs, a hybrid pattern, and concrete validation steps for order.placed handling.
31 Jan 2026, 05:04 UTC

The Core Decision
Shopware 6.4 introduced a fundamental architectural choice: extend the platform through Apps (decoupled, API-first services) or traditional Plugins (in-process Symfony bundles). This decision affects upgrade resilience, team skill requirements, latency profiles, and operational complexity. The right choice depends on whether your extension needs direct database access, sub-10ms latency, or must survive core upgrades without code changes.
Constraints That Drive the Choice
- Shopware version: Apps require 6.4+; older versions only support Plugins.
- Database write access: Apps cannot run migrations or write directly to core tables — they need a companion Plugin or manual SQL.
- Event coverage: Apps only receive webhooks for events exposed in the webhook registry (expanding each release); Plugins can subscribe to any kernel event.
- Hosting: Apps need external HTTPS endpoints (ngrok/Cloudflare Tunnel for local dev); Plugins run fully offline.
- Authentication: Apps use asymmetric JWT (RS256); misconfigured keys cause silent 401s on webhook delivery.
Quick Comparison
| Factor | App (Decoupled Service) | Plugin (Symfony Bundle) |
|---|---|---|
| Runtime | Separate process, any language | In-process PHP, shares kernel |
| Communication | Webhooks + Admin/Store API (HTTP) | DI container, events, Doctrine entities |
| Upgrade resilience | High — stable API surfaces only | Low — breaks on internal API/DB changes |
| Latency | Network round-trip (typically 5–50ms) | Sub-millisecond (in-process) |
| Fault isolation | App crash ≠ shop crash | Plugin error can bring down kernel |
| DB schema changes | Impossible — needs companion Plugin | Native migrations via Doctrine |
| Local development | Requires tunnel for webhook receipt | Fully offline, hot-reload in dev mode |
| Team skills | Any language, HTTP/JSON knowledge | PHP, Symfony, Doctrine expertise required |
| Distribution | Private or public (manifest validation) | Composer install, store requires PHP review |
Trade-offs in Practice
When to Choose Apps
Apps excel for integrations that touch external systems or must survive zero-downtime upgrades. ERP/PIM synchronization, payment gateways, external search services, headless frontends, and long-running async jobs all fit the App model. The network latency is acceptable because these operations are inherently async or human-speed. A payment gateway App, for example, only needs to respond to order.placed webhook and call the provider's API — the 20ms HTTP round-trip is negligible compared to the provider's own latency.
When to Choose Plugins
Plugins remain necessary for deep kernel integration. Custom entities with complex relationships, checkout flow modifications (e.g., custom pricing rules), ACL extensions, storefront Twig overrides, and admin UI panels all require direct access to Symfony services, Doctrine repositories, and kernel events that aren't exposed via webhooks. If your extension writes to product, order, or customer tables in the same transaction as core operations, a Plugin is the only supported path.
The Hybrid Pattern (Shopware 6.5+)
Shopware 6.5 encourages a hybrid approach: a minimal Plugin registers custom fields, entities, and migrations via manifest.xml and services.xml, while business logic lives in an App service. The App uses the App Scripts API or Custom Fields API to read/write data. This gives you upgrade-safe logic with schema ownership. Example: a Plugin defines a subscription entity and migration; the App handles Stripe webhook processing and updates the entity via API.
Concrete Validation: Minimal App vs Plugin for order.placed
This validation demonstrates the development workflow difference. Run both on a Shopware 6.5+ instance.
App Implementation
- Create scaffold:
shopware app:create --name OrderLoggerApp --path ./order-logger-app(requires Shopware CLI). - Edit
manifest.xmlto declare webhook subscription:<webhooks> <webhook name="order-placed" url="https://your-app.example.com/webhook/order-placed" event="order.placed"/> </webhooks> - Implement HTTPS endpoint (any language) that verifies
shopware-shop-signatureheader using the public key frommanifest.xml<public-key>. - Deploy to test HTTPS endpoint (ngrok for local:
ngrok http 8000). - Register in Admin → Extensions → Apps → "Add App" → upload
manifest.xml. - Place test order; verify payload arrives at your endpoint with
orderId,orderNumber,lineItems.
Plugin Implementation
- Create scaffold:
bin/console plugin:create OrderLoggerPlugin. - Add event subscriber in
src/Subscriber/OrderPlacedSubscriber.php:<?php namespace OrderLoggerPlugin\Subscriber; use Shopware\Core\Checkout\Order\Event\CheckoutOrderPlacedEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class OrderPlacedSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [CheckoutOrderPlacedEvent::class => 'onOrderPlaced']; } public function onOrderPlaced(CheckoutOrderPlacedEvent $event): void { $order = $event->getOrder(); // Direct access to $order->getId(), $order->getLineItems(), Doctrine repo $this->logger->info('Order placed', ['order' => $order->getOrderNumber()]); } } - Enable:
bin/console plugin:install --activate OrderLoggerPlugin. - Place test order; open Symfony Profiler → Events tab → verify
CheckoutOrderPlacedEventlistener executed.
Upgrade Stress Test
On a copy of production DB with both extensions installed, run:
bin/console system:upgrade --dry-run
Observe which extension requires manual migration steps. The Plugin will likely flag deprecated service references or DB schema conflicts; the App will show zero required changes if it only uses stable API endpoints.
Performance Baseline
Benchmark both approaches under load:
ab -n 1000 -c 10 https://shop.example.com/api/order
Compare p99 latency and error rates. Expect Plugin listener p99 < 5ms; App webhook p99 20–100ms depending on network and App service scaling. Under sustained load, the App isolates backpressure — a slow App service queues webhooks (Shopware retries with exponential backoff) while the shop remains responsive. A slow Plugin blocks PHP-FPM workers.
Limitations and Verification Checklist
- App webhook gaps: Not all kernel events have webhook equivalents. Check current webhook registry before committing.
- Silent auth failures: Verify
var/log/shopware.logforAppAuthenticationFailedafter any key rotation or deploy. - No DB migrations in Apps: Schema changes always need a Plugin or manual SQL — plan this upfront.
- Local dev friction: Apps require tunnel for webhook receipt; factor this into onboarding time.
- Version floor: Apps need 6.4+; if you support 6.3 or older, Plugin is mandatory.
Decision Checklist
- Does the extension need direct DB write access to core tables in the same transaction? → Plugin
- Must it survive zero-downtime core upgrades without code changes? → App
- Is sub-10ms latency critical for the hot path? → Plugin
- Team lacks PHP/Symfony expertise? → App (any language)
- Need custom admin UI panels or storefront Twig overrides? → Plugin
- Integrating with external ERP, payment, or search service? → App
- Need complex custom entities with Doctrine relationships? → Plugin (or hybrid)
If answers split, use the hybrid pattern: Plugin for schema/entities, App for business logic.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.