Custom shipping logic in Shopware 6

Shopware 6’s Rule Builder handles shipping for most shops without any custom code. Weight-based pricing, order value thresholds, country restrictions, free shipping above a certain amount. It covers the basics well, and for straightforward logistics it’s genuinely all you need.

I recently built a plugin for a client whose products ranged from 1-liter containers to 65-liter drums, shipped across 5 different package types, each with its own cost structure. Their shipping was driven by physical space, and no configuration UI can express that. This post walks through how a custom shipping cost calculation engine actually looks in production Shopware 6, and how to know whether you need one.

What Shopware gives you out of the box

Credit where it’s due: Shopware’s shipping system is flexible. The Rule Builder lets you define conditions based on weight, cart price, item count, customer group, shipping country, and more. Price matrices map those conditions to shipping costs. You can configure multiple shipping methods, each with its own set of rules.

The limitation is architectural. These tools model shipping as a lookup problem: match a set of conditions, return a price. They can’t model shipping as an optimization problem: given N items of varying physical sizes, find the cheapest combination of packages that fits everything.

When your products have radically different physical dimensions and need to be packed across multiple package types, you’ve left the territory of configuration and entered the territory of algorithms.

The shipping problem that broke the rules

The client sold physical goods in containers ranging from 1 liter to 65 liters, with some products available in oversized variants of the same volume. They shipped using 5 package types: small parcels (Paket), half palettes, Euro palettes, industrial palettes, and XXL palettes. Each package type had its own carrier cost.

The constraints were physical. A 50-liter container can’t fit in a small parcel. A customer ordering three 1-liter bottles shouldn’t have their shipment placed on a Euro palette. A mixed cart with both small and large items needs optimal allocation across package types to minimize total shipping cost.

Geography added its own layer. Germany, Austria, and international destinations each had different pricing rules. German island zip codes were blocked entirely because carriers won’t deliver palettes there. Free shipping promotions were restricted to domestic orders only.

No amount of Rule Builder configuration can express: “Pack these 14 items across the fewest packages, choosing from 5 types, while respecting physical constraints and minimizing total cost.”

Space usage as a percentage

The shift in thinking was to stop modelling products by weight or flat-rate categories, and start modelling them by what percentage of each package type they consume.

A 1-liter container uses 6.67% of a small parcel but only 0.83% of a Euro palette. A 50-liter container uses 0% of a small parcel (it doesn’t fit at all), 100% of a half palette, and 50% of a Euro palette. Each of the 17 product sizes maps to a specific space consumption percentage across all 5 package types.

This turns shipping calculation into a clear optimization: fill packages to 100% capacity using the cheapest combination of package types.

The oversized variants added another dimension. Two products with the same volume but different physical footprints consume different amounts of space. A standard 15-liter container and its oversized variant both hold 15 liters, but the oversized version takes up significantly more room in every package type.

This is a bin-packing problem. It’s NP-hard in the general case, but with a bounded set of package types and a greedy heuristic, you can get optimal-enough results in milliseconds.

The bin-packing algorithm inside Shopware’s cart pipeline

The calculation runs on every cart update and follows a greedy strategy.

  • Sort products largest-first. The biggest items are the hardest to fit, so placing them first minimizes wasted space in later rounds.
  • Check existing packages before opening new ones. If a partially-filled Euro palette has 50% capacity remaining and the next item uses 12.5%, put it there instead of opening a new package.
  • Pick the package type that minimizes total packages needed. For each product batch, the algorithm evaluates all 5 package types and selects the one requiring the fewest total packages.
  • Escalate automatically. If the parcel count exceeds 10, the algorithm switches to palettes. That threshold reflects a real carrier-cost crossover where 10+ individual parcels cost more than a single palette shipment.
  • Track remaining capacity as a percentage. Each package maintains its fill level, and subsequent items are slotted into remaining space before new packages are allocated.

The plugin implements Shopware’s CartProcessorInterface, hooking into the cart pipeline after products are resolved and before the order is finalized. Registration is a single service tag in the DI container:

<service id="MyPlugin\Core\Cart\ShippingProcessor">
    <argument type="service" id="MyPlugin\Service\DeliveryCalculationService"/>
    <argument type="service" id="monolog.logger"/>
    <tag name="shopware.cart.processor" priority="4500"/>
</service>

The calculated cost is set as a manual shipping cost override on the cart delivery, replacing Shopware’s native calculation entirely. The full package breakdown (how many of each type) is stored as a cart extension so it persists into the order record. The warehouse team uses this breakdown to know exactly which packages to prepare.

Geographic rules and edge cases

Shipping logic is never only about packaging. Geography adds its own layer of complexity.

German island zip codes are loaded from a curated dataset at calculation time. If the customer’s shipping address matches one of these zip codes, the order is blocked with a clear error message. Carriers won’t deliver palettes to islands like Sylt or Helgoland, and attempting it would result in failed deliveries and return shipping costs.

International orders are forced into parcel-only mode, falling back to Shopware’s native shipping price configuration. The space optimization runs only for domestic German shipments where the full range of package types is available.

Free shipping promotions are restricted to domestic orders. Products flagged as free-shipping still count toward the package calculation (the warehouse still needs to pack and ship them), but their cost is excluded from the total. Attempting to order free-shipping products internationally triggers a blocking error so the promotion can’t silently eat into margins.

Every shipping plugin I’ve built has at least one rule that sounds absurd until you understand the logistics behind it.

When to build a custom shipping engine

Not every shop needs a custom shipping engine. Here’s the decision framework I use.

  • If your shipping logic can be expressed as “if condition, then price,” use the Rule Builder. It’s what it’s designed for, and maintaining configuration is cheaper than maintaining code.
  • If your logic involves optimization (minimize packages), physical constraints (item X can’t fit in container Y), or algorithmic decisions (escalate when a threshold is exceeded), you need custom code.

The good news is that Shopware’s architecture supports this cleanly. CartProcessorInterface exists for exactly this purpose. Custom entities for package types and their costs let the client adjust pricing through the admin panel without touching code. You’re extending the framework at the exact point where configuration runs out.

Frequently asked questions

When do I need a custom shipping plugin in Shopware 6? When your shipping logic involves optimization (minimize packages), physical constraints (item X doesn’t fit in container Y), or algorithmic decisions (escalate above a threshold). If it can be expressed as “if condition, then price,” the Rule Builder is the right tool.

What is CartProcessorInterface in Shopware 6? It’s the extension point in Shopware 6’s cart pipeline for custom cart logic. It runs after line items are resolved and before the order is finalized, which makes it the right place to inject custom shipping cost calculation, custom discounts, or virtual line items.

Can Shopware 6 calculate shipping costs by volume? Out of the box, no. The Rule Builder models shipping as a lookup: match conditions, return price. Volume-based shipping requires a custom CartProcessor that models products as percentage space consumption across package types and runs a bin-packing algorithm.

Is bin-packing really NP-hard for shipping calculations? The general bin-packing problem is NP-hard. In practice, with a bounded set of package types and a greedy largest-first heuristic, an implementation returns optimal-enough results in milliseconds, which is what production shipping calculations need.

How do I block shipping to German island zip codes in Shopware 6? The Rule Builder can block a shipping method by zip code, but maintaining a full list of German island zip codes there is fragile. Loading the list from a curated dataset inside a custom CartProcessor, and blocking the order with a clear error message, is more maintainable and easier to update when carrier coverage changes.

I wrote about the broader architecture rules I follow in my Shopware plugin development approach. The same engine powers the automated dropshipping pipeline I built for the same client, where the package breakdown feeds directly into supplier order routing. More examples are in my case studies.

If your shipping logic has outgrown what the admin panel can express, get in touch.

Share this article

Found this useful? Share it with your network

Huzaifa Mustafa

Huzaifa Mustafa

Shopware 6 certified developer with 164+ custom plugins delivered and 96+ clients across the DACH region. I write about Shopware architecture, e-commerce performance, and lessons from real projects.

Need help with Shopware?

Let's discuss how I can help with your e-commerce project.