<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Javier Benito — Blog</title>
    <link>https://javierbenito.me/blog/</link>
    <description>Field notes on Spryker and Symfony commerce platforms, integrations, migrations and AI-assisted delivery.</description>
    <language>en</language>
    <copyright>© Artevit — Javier Benito Santoni</copyright>
    <atom:link href="https://javierbenito.me/feed.xml" rel="self" type="application/rss+xml"/>
    <lastBuildDate>Tue, 28 Jul 2026 09:00:00 GMT</lastBuildDate>
    <item>
      <title>What AI-assisted delivery actually changes on a commerce build</title>
      <link>https://javierbenito.me/blog/what-ai-assisted-delivery-actually-changes/</link>
      <guid isPermaLink="true">https://javierbenito.me/blog/what-ai-assisted-delivery-actually-changes/</guid>
      <pubDate>Tue, 28 Jul 2026 09:00:00 GMT</pubDate>
      <description>Not &quot;AI writes the code&quot;. The measurable shift is in the work around the code — boilerplate, test scaffolding, migration sweeps and review throughput — and the review discipline that has to come with it.</description>
      <category>ai-assisted delivery</category>
      <category>spryker</category>
      <content:encoded><![CDATA[<p>The pitch most agencies are making right now is that AI writes the code and the project gets cheaper. That is not what happens on a real Spryker or Symfony build, and pretending otherwise sets up a client for a bad surprise around week six.</p>
<p>What actually changes is narrower and more useful: the ratio of time spent on mechanical work versus decisions. On a typical enterprise commerce build, a depressing share of engineering hours goes to work that is not hard, just long — module scaffolding, transfer object definitions, data importers, test fixtures, mapping one system&#39;s vocabulary onto another&#39;s. That is where the leverage is.</p>
<h2 id="the-three-places-it-pays">The three places it pays</h2>
<p><strong>Boilerplate with a known shape.</strong> Spryker&#39;s module structure is highly regular: a new module means a bundle of files whose contents are almost fully determined by the domain object. Generating that from a specification and reviewing the result is faster than typing it, and the review is genuinely quick because the shape is familiar. The same applies to Symfony form types, doctrine mappings, and API resource definitions.</p>
<p><strong>Migration sweeps.</strong> Applying one mechanical change across two hundred call sites is the canonical case. A deprecation, a renamed transfer property, a changed signature. The work is tedious enough that engineers batch it, defer it, and let it rot. When the sweep is cheap, it happens on schedule.</p>
<p><strong>Test scaffolding.</strong> Not the assertions that matter — the fixtures, the builders, the setup that makes writing the meaningful test bearable. Teams under-test integration code because standing up the fixture costs more than the test. Lower that cost and coverage moves without anyone being told to care about coverage.</p>
<p>Notice what is missing from that list: the architecture, the integration contracts, the pricing rules, the decision about where a boundary goes. Those are the parts that were always the job, and they are still the job.</p>
<h2 id="the-review-discipline-is-the-whole-product">The review discipline is the whole product</h2>
<p>Generated code that nobody reads is a liability with a faster clock speed. The practice that makes this work is unremarkable and non-negotiable:</p>
<ul><li>Every generated change goes through the same review as hand-written code. No separate lane, no &quot;it&#39;s just boilerplate&quot; exemption.</li><li>The reviewer is the engineer who will own the code in production. Not the person who prompted for it.</li><li>Anything touching money, tax, availability or personal data gets read line by line, twice. The cost of a plausible-looking bug in a pricing rule is not symmetric with the cost of reading it.</li></ul>
<p>The failure mode is not that the model writes nonsense — modern ones rarely do. It is that it writes something reasonable that quietly disagrees with a business rule nobody wrote down. That failure is invisible to tests generated from the same misunderstanding, which is why &quot;the tests pass&quot; carries less weight here than it used to.</p>
<h2 id="a-worked-example">A worked example</h2>
<p>Here is the kind of thing that looks fine and is wrong. A generated price resolver for a B2B catalog:</p>
<pre><code class="language-php">public function resolve(ProductTransfer $product, CustomerTransfer $customer): Money
{
    $contract = $this-&gt;contractReader-&gt;findActive($customer);

    if ($contract !== null) {
        return $contract-&gt;getPriceFor($product);
    }

    return $product-&gt;getDefaultPrice();
}</code></pre>
<p>Idiomatic, typed, readable, and it will pass a review that is checking whether the code is well-formed. It is also wrong in most B2B contexts, because contract price and list price are not alternatives — the customer pays the lower of the two, and in some jurisdictions a contract price above list is not merely a bad deal but unenforceable. No amount of static analysis finds that. Someone who knows the domain reading it does, in about four seconds.</p>
<p>That four seconds is the job now.</p>
<h2 id="what-it-means-for-an-estimate">What it means for an estimate</h2>
<p>The honest version of the economics: mechanical work compresses substantially, decision work does not compress at all, and review work goes <em>up</em> — you are reviewing more code per unit of calendar time than before.</p>
<p>On builds where the mechanical share was large — a fresh Spryker implementation with many modules, or a broad integration surface — that nets out to a meaningfully shorter delivery. On builds that are mostly gnarly business rules against a legacy integration, it barely moves the total, and anyone promising otherwise is selling you an estimate rather than a plan.</p>
<p>The useful question to ask a supplier is not whether they use AI. It is which of their hours are mechanical, and who reads the output before it ships.</p>]]></content:encoded>
    </item>
    <item>
      <title>Where Spryker storefronts actually get slow</title>
      <link>https://javierbenito.me/blog/where-spryker-storefronts-actually-get-slow/</link>
      <guid isPermaLink="true">https://javierbenito.me/blog/where-spryker-storefronts-actually-get-slow/</guid>
      <pubDate>Tue, 14 Jul 2026 09:00:00 GMT</pubDate>
      <description>It is rarely PHP. Publish and Sync backlogs, chatty Redis reads, and the synchronous call hiding in checkout - a field guide to real Spryker performance work.</description>
      <category>spryker</category>
      <category>performance</category>
      <content:encoded><![CDATA[<p>When a Spryker storefront is slow, the first instinct is to profile PHP. It is almost never the productive place to look. Spryker&#39;s architecture already did the big performance move for you — Yves reads denormalised data from Redis and Elasticsearch instead of joining tables at request time — so when things get slow anyway, it is usually because something is working against that design rather than within it. After enough of these engagements, the culprits form a short, repeatable list.</p>
<h2 id="publish-sync-fast-reads-bought-on-credit">Publish &amp; Sync: fast reads, bought on credit</h2>
<p>The reason product pages can be fast is that the work happened earlier: an entity changes in Zed, events fire, publishers rebuild the denormalised view, and sync writes it to Redis and Elasticsearch. That pipeline is the platform&#39;s load-bearing wall, and two failure modes account for most of the grief.</p>
<p><strong>The backlog.</strong> A full catalog import touches every product, which fires events for every product, which can leave the queues chewing for hours — during which the storefront cheerfully serves stale prices and yesterday&#39;s stock. If your nightly import &quot;sometimes isn&#39;t finished by morning&quot;, this is usually what that sentence means. The fixes are unglamorous: import true deltas instead of the whole feed, batch the events, tune publisher chunk sizes, and put queue depth on a dashboard with an alert. Sync lag is invisible until a customer orders something that sold out yesterday — it deserves a metric more than most things that have one.</p>
<p><strong>The over-eager publisher.</strong> A custom publisher that rebuilds too much per event — the classic is touching every abstract product in a category because one attribute changed — multiplies a small edit into thousands of writes. Reading the publisher code with the question &quot;what is the blast radius of one event?&quot; is a profitable afternoon on most mature Spryker projects.</p>
<h2 id="redis-round-trips-death-by-a-thousand-gets">Redis round trips: death by a thousand GETs</h2>
<p>Redis answers in well under a millisecond, which tempts people into treating it as free. It is not free from PHP, because each lookup pays the full round trip. A product listing that resolves each tile&#39;s data one key at a time — product, then price, then availability, then image sets, then labels — quietly stacks up hundreds of sequential network hops, and the page spends more time waiting on the loop than on any single query.</p>
<p>The remedies are old-fashioned: batch key lookups (<code>MGET</code>) wherever a collection is being hydrated, and treat any per-item storage read inside a <code>foreach</code> as a code smell in review. The storage client&#39;s batch methods exist precisely for this; the sin is usually in custom code that never heard of them. The same shape applies to Elasticsearch — one query with facets and the result page, not a query per widget.</p>
<p>While you are in the neighbourhood, look at what is <em>in</em> those keys. I have seen product storage entries north of a hundred kilobytes because a publisher serialised the entire attribute universe &quot;to be safe&quot;. You pay to build it, store it, transfer it and unserialise it, on every page, forever. Publish what the frontend reads. Nothing else.</p>
<h2 id="the-synchronous-call-hiding-in-checkout">The synchronous call hiding in checkout</h2>
<p>Browse and search run on Spryker&#39;s fast path. Checkout is where projects smuggle in the slow one: a live ERP credit check, a real-time stock reservation, a tax service, a payment provider&#39;s session setup — each a reasonable decision, each adding a synchronous external dependency to the single most conversion-sensitive click in the shop. Three reasonable decisions later, &quot;place order&quot; takes nine seconds and fails whenever any one of three other systems sneezes.</p>
<p>The design questions worth forcing early: which of these answers must be <em>correct at this instant</em>, and which can be seconds stale? Stock can usually be a cached read with a reservation behind the scenes; the credit check often genuinely must be live — in which case it needs a timeout, a fallback policy agreed with finance, and a place in the order state machine for &quot;accepted pending review&quot;. What it must never be is an unbounded wait between the customer and their confirmation page.</p>
<h2 id="measure-like-a-customer-not-like-a-server">Measure like a customer, not like a server</h2>
<p>The last recurring failure is measuring the wrong number. Server-side response time on a cached product page tells you almost nothing about what the customer felt — front-end weight, third-party tags and cache hit rates routinely dominate. Two habits keep the work honest: track percentiles from real-user monitoring rather than averages from synthetic checks against warm caches, and before any tuning sprint, write down the one journey that matters (usually listing → product → cart → order) with its current p95 numbers. Optimising pages nobody visits is the most popular performance work there is, because it is easier than fixing the import.</p>
<p>None of this is exotic engineering. It is knowing which parts of the machine carry the load, and pointing the effort there instead of at the profiler&#39;s prettiest flame graph.</p>]]></content:encoded>
    </item>
    <item>
      <title>Taking over the codebase nobody wants to touch</title>
      <link>https://javierbenito.me/blog/taking-over-the-codebase-nobody-wants-to-touch/</link>
      <guid isPermaLink="true">https://javierbenito.me/blog/taking-over-the-codebase-nobody-wants-to-touch/</guid>
      <pubDate>Tue, 30 Jun 2026 09:00:00 GMT</pubDate>
      <description>A working sequence for legacy rescue - a deploy pipeline before any bug fix, characterisation tests before any refactor, and why month one ships almost no features.</description>
      <category>legacy</category>
      <category>symfony</category>
      <content:encoded><![CDATA[<p>The call is always some version of the same story. The shop makes real money. The agency that built it is gone, or the one developer who understood it left in March. There is a backlog of bugs the business has learned to live with, a Symfony version that stopped receiving security fixes a while ago, and a deployment process that involves a specific person, a specific laptop, and courage.</p>
<p>The instinct — the client&#39;s and the engineer&#39;s — is to start fixing the most painful bug on day one. It is almost always the wrong first move, because in a codebase you do not understand, with no tests and no safe deployment, every fix is a coin flip that can convert &quot;annoying bug&quot; into &quot;site down&quot;. Here is the sequence that has worked instead, and it front-loads a month in which embarrassingly few features ship.</p>
<h2 id="week-one-make-deployment-boring">Week one: make deployment boring</h2>
<p>Whatever else is true of the system, you cannot operate it until you can release and roll back without ceremony. So the first deliverable is not a fix, it is a pipeline: reproducible builds from a clean checkout, one command to deploy, one command to roll back, and the environment configuration pulled out of the special laptop and into version control (secrets excepted, obviously).</p>
<p>This work is invisible to the business, so say the quiet part in the kickoff: <em>nothing user-visible will improve for a few weeks, and this is the part of the engagement that makes every later week cheaper.</em> Clients accept a stated trade-off far more gracefully than a discovered one.</p>
<p>While the pipeline work happens, resist knowledge-transfer theatre. Reading the entire codebase up front converts none of it into understanding. Instead, wire up error tracking and slow-query logging and let production tell you where the bodies are. A week of real traffic annotates the codebase better than its authors ever did — you get a ranked list of what actually breaks and what is actually slow, which is the only map worth having.</p>
<h2 id="characterise-before-you-change">Characterise before you change</h2>
<p>The standard objection to touching legacy code is &quot;there are no tests&quot;. True, and unhelpful — you cannot retrofit a proper unit test suite onto code whose behaviour you do not yet know is <em>intended</em>. What you can do is pin the current behaviour down before changing it. Two tools carry most of the weight:</p>
<ul><li><strong>Golden-master tests for the money paths.</strong> Drive checkout, cart totals, tax and shipping calculation through their paces with a few dozen recorded inputs and assert the outputs stay byte-identical. You are not asserting the behaviour is correct — nobody knows — you are asserting your changes did not alter it unknowingly. When a golden master breaks on purpose, that diff is the code review.</li><li><strong>HTTP-level smoke tests.</strong> Twenty URLs, logged in and out: status codes, a handful of load-bearing strings, response times. Crude, and it catches the class of regression that actually takes revenue down — the fatal error on a template path nobody clicked in staging.</li></ul>
<p>With the pipeline and the pins in place, upgrades become mechanical instead of heroic. The Symfony jump goes minor version by minor version, deprecations cleared at each step, golden masters green at each step — tedious, low-drama work, which is the point. (This staircase of mechanical sweeps is also where AI-assisted tooling genuinely pays; the deprecation churn compresses nicely. The judgement calls on either side of it do not.)</p>
<h2 id="strangle-the-worst-of-it-keep-the-rest">Strangle the worst of it; keep the rest</h2>
<p>Somewhere in month two, someone will propose the rewrite. The honest response to &quot;this is unmaintainable&quot; is usually &quot;parts of this are unmaintainable&quot; — most of the revenue is flowing through code that, however ugly, encodes years of fixes for problems you have not met yet. Throwing that away wholesale means rediscovering those problems one incident at a time.</p>
<p>So rescue proceeds by organ transplant, not resurrection. The genuinely rotten subsystem — in one memorable case, a homegrown &quot;cache&quot; that wrote serialized PHP objects to the database and was the source of a quarter of all errors — gets isolated behind an interface, rebuilt, and swapped, golden masters standing guard. The merely old-fashioned code gets left alone until there is a business reason to open it. Ugly-but-stable is not a technical problem; it is a preference, and rescues that chase preferences run out of budget before they run out of ugliness.</p>
<p>The success condition is worth stating because it is modest: the client can deploy on a Tuesday afternoon without holding their breath, the error tracker is quiet enough that a new entry means something, and the next developer to join gets productive in days. Not a beautiful codebase — a boring one. In legacy work, boring is the deliverable.</p>]]></content:encoded>
    </item>
    <item>
      <title>Replatforming without freezing the roadmap</title>
      <link>https://javierbenito.me/blog/replatforming-without-freezing-the-roadmap/</link>
      <guid isPermaLink="true">https://javierbenito.me/blog/replatforming-without-freezing-the-roadmap/</guid>
      <pubDate>Tue, 16 Jun 2026 09:00:00 GMT</pubDate>
      <description>The strangler pattern applied to commerce — how to move a storefront onto Spryker while the old system keeps taking orders and the business keeps shipping features.</description>
      <category>spryker</category>
      <category>migration</category>
      <content:encoded><![CDATA[<p>Every replatforming conversation starts the same way. The current storefront is eight years old, nobody wants to touch the checkout, and the CTO has been told the migration will take nine months. The unspoken assumption in that estimate is that the roadmap stops for nine months.</p>
<p>It doesn&#39;t have to. The business case for replatforming usually evaporates if you have to buy it with three quarters of frozen feature work, so the interesting question is never &quot;how do we rebuild this?&quot; — it&#39;s &quot;how do we rebuild this while the old system keeps taking orders?&quot;</p>
<h2 id="route-traffic-before-you-move-code">Route traffic before you move code</h2>
<p>The single decision that makes or breaks an incremental migration is where the routing boundary lives. Put a reverse proxy in front of both systems on day one, before a single line of the new platform exists. Every path resolves to the legacy application by default; you move paths across one at a time.</p>
<pre><code class="language-nginx">location /checkout { proxy_pass http://legacy; }
location /catalog  { proxy_pass http://spryker; }
location /         { proxy_pass http://legacy; }</code></pre>
<p>That is the entire strangler pattern. It is unglamorous, and it is what turns a big-bang cutover into a sequence of reversible decisions. When the new catalog misbehaves under real traffic, the rollback is one line of config, not a weekend.</p>
<p>The corollary matters just as much: <strong>pick the boundary along a seam the business already understands.</strong> Catalog, search, checkout, account. If your first slice cuts across three of those, you have not found a seam — you have found a rewrite.</p>
<h2 id="sequence-by-risk-not-by-ease">Sequence by risk, not by ease</h2>
<p>The instinct is to migrate the easy things first for an early win. That gets the sequence backwards. The parts of a commerce platform that carry migration risk are the ones with the most integration surface — pricing rules, tax, availability, the ERP handshake — and they are exactly the parts you want to discover problems in while you still have budget and political capital.</p>
<p>A sequence that has held up across several builds:</p>
<ol><li><strong>Catalog and search first.</strong> Read-heavy, low blast radius, and it forces you to solve product data import and the PIM relationship immediately.</li><li><strong>Content and landing pages next.</strong> Marketing gets visible value early, which buys patience for the quieter phases.</li><li><strong>Cart and pricing after that.</strong> This is where the real business rules live, and where the legacy system&#39;s undocumented behaviour surfaces.</li><li><strong>Checkout and payment last.</strong> Highest risk, most regulated, and by this point you understand the domain well enough to do it once.</li></ol>
<p>Accounts and order history sit awkwardly across the whole sequence, because customers expect continuity. Plan for a period where both systems read from one identity source — usually the legacy one — rather than trying to migrate identity in the same slice as anything else.</p>
<h2 id="the-dual-write-period-is-the-hard-part">The dual-write period is the hard part</h2>
<p>For any slice touching writes, there is a window where both systems need a consistent view of the same data. Teams reach for dual-write and then discover that dual-write without an ordering guarantee is just two systems disagreeing at speed.</p>
<p>Publish domain events to a log — Kafka is the usual answer, and it is the right one — and let both systems consume from it. The legacy application gets a small outbound adapter, the new platform gets a consumer, and the log becomes the arbiter of what happened and in what order. This is more work upfront than a synchronous double-write, and it is the difference between a migration you can reason about and one you can only apologise for.</p>
<blockquote><p>The question to ask about any dual-write design: when the two systems disagree, which one is wrong, and how do you find out before a customer does?</p></blockquote>
<h2 id="what-actually-goes-wrong">What actually goes wrong</h2>
<p>Not the things in the risk register. In practice:</p>
<ul><li><strong>Redirects.</strong> Nobody budgets for them, and a botched URL map costs more organic traffic than the replatform gains in conversion. Build the redirect map from the server logs, not from the sitemap — the sitemap does not know about the ten-year-old campaign URLs still earning links.</li><li><strong>Search relevance.</strong> The new search is objectively better and converts worse for six weeks, because merchandisers had years of tuning baked into the old one. Budget for re-tuning as a project, not as a task.</li><li><strong>Reporting.</strong> Finance has a spreadsheet that reads from a database view somebody built in 2017. It will be discovered two days before cutover.</li></ul>
<p>None of these are engineering problems, which is exactly why they get missed by engineering-led migration plans.</p>
<h2 id="when-a-big-bang-cutover-is-right">When a big-bang cutover is right</h2>
<p>Incremental is not always correct. If the storefront is small, the integration surface is thin, and the legacy system genuinely cannot be fronted by a proxy — some hosted platforms make this impossible — then the proxy layer is real work that buys you nothing you can&#39;t get from a well-rehearsed weekend cutover.</p>
<p>The honest test: if the incremental path costs more than about a fifth of the total migration budget in pure plumbing, and the rollback story for a big-bang is credible, take the big-bang. Rehearse it three times against production data first.</p>]]></content:encoded>
    </item>
    <item>
      <title>B2B checkout is not B2C with invoices</title>
      <link>https://javierbenito.me/blog/b2b-checkout-is-not-b2c-with-invoices/</link>
      <guid isPermaLink="true">https://javierbenito.me/blog/b2b-checkout-is-not-b2c-with-invoices/</guid>
      <pubDate>Tue, 12 May 2026 09:00:00 GMT</pubDate>
      <description>Payment terms, credit limits, approval chains and punchout — what actually changes in a B2B checkout, and where the Spryker defaults end and your build begins.</description>
      <category>b2b</category>
      <category>spryker</category>
      <category>payments</category>
      <content:encoded><![CDATA[<p>The most expensive assumption in B2B commerce is that checkout is the solved part. The reasoning goes: B2C checkouts are a commodity, B2B is B2C with invoices instead of credit cards, therefore the checkout is a line item. Then the workshops start, and the line item unfolds into the half of the project where the client&#39;s actual business lives.</p>
<p>Here is what is really inside it, and roughly where a platform like Spryker stops helping and your build begins.</p>
<h2 id="payment-mostly-means-credit">&quot;Payment&quot; mostly means credit</h2>
<p>In B2C, payment is authorisation: is this card good for €80, yes or no, in two seconds. In B2B, the dominant payment method is <em>on account</em> — the order ships now and the invoice is due in thirty days — which means the question is not &quot;is the card good&quot; but &quot;is this customer good for another €40,000 on top of what they already owe&quot;. That is a credit decision, and the data to make it lives in the ERP: open items, credit limit, payment history, blocks.</p>
<p>So the checkout has a synchronous dependency on the credit check, and you need three answers designed before anyone writes code:</p>
<ol><li><strong>Where is the check made?</strong> Asking the ERP live is accurate and couples your checkout availability to the ERP&#39;s uptime. Replicating exposure data into the shop is fast and can be minutes stale — which matters exactly when a customer is placing orders rapidly, which is exactly when credit matters.</li><li><strong>What happens on &quot;no&quot;?</strong> Hard-blocking the order loses revenue over data staleness. The pattern that survives contact with sales teams: accept the order into a review state, tell the customer honestly, and give the credit team a queue. A blocked checkout is a phone call to sales anyway; better to own it in the system.</li><li><strong>What happens on &quot;unknown&quot;?</strong> The ERP will be unreachable during checkout at some point. Failing open or failing closed is a finance-department decision. Get it in writing; you are choosing between lost orders and bad debt.</li></ol>
<p>Spryker gives you the plumbing here — a payment method plugin architecture and a state machine per payment method — and none of the policy. The state machine you configure <em>is</em> the credit policy. Treat that XML with the reverence you would give pricing code.</p>
<h2 id="the-order-is-a-committee">The order is a committee</h2>
<p>A B2C order has one human. A B2B order routinely has three: the requester who fills the cart, the approver who owns the budget, and purchasing who owns the supplier relationship. Spryker&#39;s company account model — business units, roles, permissions, approval flows — covers the healthy 80%: spend thresholds per role, approve or reject with a comment, notifications.</p>
<p>The remaining 20% is where the workshops earn their keep, because approval rules encode the org chart, and org charts are weird. Approval by cost centre rather than amount. Deputies for approvers on holiday. The rule that chemicals need a safety officer&#39;s sign-off regardless of price. You will not configure that; you will build it. The trick is to find out <em>which</em> of those rules are real requirements and which are how the old fax workflow happened to work. Asking &quot;what happens today when this rule is violated?&quot; kills about half of them.</p>
<h2 id="punchout-quotes-and-the-other-doors-into-checkout">Punchout, quotes, and the other doors into checkout</h2>
<p>A serious B2B storefront has more entrances than the cart button:</p>
<ul><li><strong>Punchout (OCI/cXML).</strong> Your biggest customers may never see your checkout at all — their procurement system opens your catalog, the filled cart is returned to <em>their</em> system, and the order arrives later as an EDI or API message. Checkout, payment and approval all happen on their side. If the target segment includes enterprises with SAP Ariba or Coupa, punchout is not an add-on, it reshapes what &quot;checkout&quot; even means for your best accounts.</li><li><strong>Quotes.</strong> Above a certain order value, the cart is an opening bid. Request-for-quote flows — cart becomes a quote, sales adjusts prices, the customer converts it back into an order — blur the line between commerce platform and CRM. Decide early which system owns a negotiated price and how long it is honoured.</li><li><strong>Order templates and replenishment.</strong> The weekly re-order of two hundred line items is the actual daily life of B2B buying. A checkout optimised for the first-time visitor and unusable for the two-hundred-line repeat order has optimised the wrong funnel.</li></ul>
<p>None of this is exotic. It is the ordinary contents of the phrase &quot;B2B checkout&quot;, visible only if you unfold it before the estimate instead of after. The projects that go well are the ones where the credit policy, the approval matrix and the punchout question were on a whiteboard in week two — while the projects that go badly discovered them in UAT, one surprised stakeholder at a time.</p>]]></content:encoded>
    </item>
    <item>
      <title>Product data is the launch date</title>
      <link>https://javierbenito.me/blog/product-data-is-the-launch-date/</link>
      <guid isPermaLink="true">https://javierbenito.me/blog/product-data-is-the-launch-date/</guid>
      <pubDate>Tue, 07 Apr 2026 09:00:00 GMT</pubDate>
      <description>Most &quot;development delays&quot; on commerce launches are product data delays in disguise. What a PIM like Akeneo actually fixes — and the part it can't.</description>
      <category>pim</category>
      <category>integrations</category>
      <content:encoded><![CDATA[<p>Ask why a commerce launch slipped and you will hear about development. Look at the actual critical path and, more often than not, the code was waiting on the data. The catalog import ran clean on the fifty hand-groomed test products and fell over on the real twelve thousand. Half the technical attributes needed for faceting exist only in a PDF the manufacturer sends yearly. Translations are &quot;in progress&quot; in the sense that an intern has the spreadsheet open.</p>
<p>None of this shows up in an engineering estimate, because it is not engineering work. It shows up in the engineering <em>timeline</em>, because the storefront cannot be finished — not styled, not tuned, not tested — against data that does not exist yet.</p>
<h2 id="why-the-demo-lies">Why the demo lies</h2>
<p>Every platform demo runs on curated data: complete attributes, pretty images, clean categories. Real catalogs have products with three parents, attributes whose meaning changed in 2019 without anyone renaming them, and a &quot;colour&quot; field containing values like <code>blue/darkblue (new)</code>. The storefront features that sell the project — faceted search, comparison tables, rich variant switchers — are exactly the features that quietly degrade into empty boxes when the attributes behind them are 60% filled.</p>
<p>That 60% is the number to chase down early. Pick the twenty attributes your storefront design actually depends on, measure fill rates across the real catalog on week one, and put the number in the steering meeting. It reframes the conversation from &quot;when will dev be done&quot; to &quot;when will the data be launchable&quot;, which is the true question.</p>
<h2 id="what-a-pim-actually-buys-you">What a PIM actually buys you</h2>
<p>Teams reach for a PIM — Akeneo, in most of the projects I see — expecting it to solve data quality. It does not. What it solves is data <em>workflow</em>, which is upstream of quality:</p>
<ul><li><strong>One place where enrichment happens</strong>, instead of the ERP for logistics fields, a spreadsheet for marketing copy, and the shop&#39;s admin for whatever was forgotten. The shop stops being an editing surface, which is the single biggest simplification available.</li><li><strong>Completeness as a first-class number.</strong> Akeneo&#39;s completeness model — required attributes per channel and locale — turns &quot;is the catalog ready&quot; from an opinion into a dashboard. Product families below threshold do not export.</li><li><strong>Translations as a process</strong>, with locales that inherit and fall back, instead of column pairs in a spreadsheet named <code>FINAL_v3_reviewed</code>.</li></ul>
<p>What it cannot buy you is the enrichment itself. Somebody still writes the copy, sources the images, and decides whether <code>blue/darkblue (new)</code> is one value or two. A PIM makes that work visible and assignable. It does not make it happen. Budget the people, not just the licence.</p>
<h2 id="the-import-pipeline-is-production-code">The import pipeline is production code</h2>
<p>The connector between the PIM and Spryker gets built as a throwaway script surprisingly often, considering it runs every night for the life of the platform. It deserves the same engineering as checkout:</p>
<ol><li><strong>Validate at the boundary, reject loudly.</strong> A product with a broken category reference should land in an error report with a reason, not silently vanish from the shop. Silent drops get discovered by the sales team, weeks later, one SKU at a time.</li><li><strong>Import deltas, but rehearse the full load.</strong> The nightly delta keeps the day-to-day cheap. The full re-import is your disaster recovery and your go-live path, and if it has not run in three months it does not work.</li><li><strong>Make throughput a tested number.</strong> Spryker&#39;s data import and Publish &amp; Sync machinery is batch-friendly, but a naive per-product loop with an event storm behind it will turn twelve thousand products into a nine-hour night. You want to know that in week four, not on cutover weekend.</li></ol>
<h2 id="sequence-the-work-honestly">Sequence the work honestly</h2>
<p>The planning mistake is treating data as a workstream that runs &quot;in parallel&quot; with the build, which in practice means nobody looks at it until the storefront needs it. The sequence that works inverts the usual order: model the catalog and stand up the real import in the first weeks, against the ugliest data you have, and let the storefront be built on top of real products from day one.</p>
<p>It feels slower, because the first sprint produces importers instead of pages. It is faster, because the alternative is discovering the catalog&#39;s actual shape during the final integration phase — which is the most expensive possible moment — and because every screenshot after week three shows the client their own products, not lorem ipsum in a hoodie.</p>
<p>The launch date is not set by the last feature merged. It is set by the day the data is fit to sell with. Treat that as the deadline and staff it like one.</p>]]></content:encoded>
    </item>
    <item>
      <title>The ERP integration is the project</title>
      <link>https://javierbenito.me/blog/the-erp-integration-is-the-project/</link>
      <guid isPermaLink="true">https://javierbenito.me/blog/the-erp-integration-is-the-project/</guid>
      <pubDate>Tue, 24 Feb 2026 09:00:00 GMT</pubDate>
      <description>The storefront is the easy half. Field notes on wiring Spryker to SAP — pricing, stock, order export, and why the middleware question decides your timeline.</description>
      <category>spryker</category>
      <category>integrations</category>
      <category>erp</category>
      <content:encoded><![CDATA[<p>Enterprise commerce projects get estimated as a storefront build with an integration line item. On every build I have been close to, the ratio was the other way around: the storefront was predictable, and the ERP integration was where the calendar went. If you are scoping a Spryker implementation against SAP — or Navision, or an AS/400 that predates the word &quot;ecommerce&quot; — treat the integration as the project and the storefront as the deliverable that falls out of it.</p>
<h2 id="decide-who-owns-each-field-in-writing">Decide who owns each field, in writing</h2>
<p>Every integration problem I have debugged at two in the morning reduces to the same root cause: two systems both believed they owned a piece of data. Prices maintained in the ERP and &quot;temporarily&quot; overridden in the shop. Stock corrected in the warehouse system and cached optimistically in the storefront. A customer record edited on both sides of the fence in the same afternoon.</p>
<p>The fix is boring and organisational, not technical. Before any mapping work starts, produce a table: every entity, every field, one owning system, one direction of flow. Products flow from the ERP (or the PIM) to the shop, never back. Orders flow from the shop to the ERP, never back — the ERP sends order <em>status</em>, which is a different object. Anyone who wants an exception to the table has to argue for it in a meeting, which is exactly as hard as it should be.</p>
<h2 id="pricing-is-where-b2b-integrations-earn-their-reputation">Pricing is where B2B integrations earn their reputation</h2>
<p>B2C pricing replication is a nightly file and a sanity check. B2B pricing against SAP means condition records — customer-specific prices, quantity scales, date-bounded agreements, sometimes thousands of combinations per account. You have two honest options:</p>
<ol><li><strong>Replicate.</strong> Flatten the condition logic into Spryker&#39;s price dimensions and import the result. Fast storefront, and a re-implementation of SAP pricing that will drift from the original the first time someone adds a condition type nobody told you about.</li><li><strong>Ask SAP at cart time.</strong> Always correct, and now your cart&#39;s response time includes a round trip to a system that was sized for batch jobs, with a failure mode in the middle of checkout.</li></ol>
<p>The pattern that has held up: replicate list and contract prices for browsing, and make one synchronous pricing call when the cart actually matters — on cart review, not on every quantity change. Cache the response against the cart hash. And decide <em>now</em> what the storefront shows when that call times out, because it will, and &quot;spinner forever&quot; is a decision too, just a bad one.</p>
<h2 id="the-order-export-is-a-distributed-system-own-up-to-it">The order export is a distributed system, own up to it</h2>
<p>Posting an order into an ERP is not a REST call, it is a small distributed systems problem wearing a business suit. The ERP will be down for maintenance windows the project plan never heard of. It will accept an order and time out on the acknowledgement. It will reject an IDoc for a reason that is true but useless, like a sales area mismatch on a customer who ordered fine last week.</p>
<p>Minimum viable honesty here is an outbox: the shop commits the order and an export job in one transaction, a worker retries with backoff, and every export carries an idempotency key so a retry after a lost acknowledgement does not book the revenue twice. I have seen the double-booked version. Finance finds it at month end, and they do not describe it as an engineering detail.</p>
<blockquote><p>The test for any order export design: kill the connection after the ERP has accepted the order but before your system hears about it. If the answer involves a human comparing spreadsheets, the design is not done.</p></blockquote>
<h2 id="the-middleware-question">The middleware question</h2>
<p>Somebody will propose an iPaaS — Mulesoft, Boomi, SAP&#39;s own integration suite — and somebody else will propose &quot;just build it in the shop&quot;. Both are wrong as defaults. The iPaaS earns its licence when there are many systems and the mappings are genuinely configuration; it becomes an expensive place to hide business logic when there is really only one integration that matters. Direct integration is fine for one system and gets quadratically worse as systems join.</p>
<p>The question that actually decides it: <strong>who will change the mapping in three years, and what tools do they know?</strong> An integration layer nobody on the retained team can operate is technical debt with a subscription fee.</p>
<p>Whichever way it goes, put contract tests on the boundary. A fixture file of real (anonymised) IDocs and API payloads, replayed in CI against your mapping code. When the ERP team upgrades and a segment quietly changes meaning, you want a red build, not a warehouse full of wrong picks.</p>
<p>The storefront demo is what sells the project. The field-ownership table, the outbox, and the fixture suite are what let you sleep after it launches.</p>]]></content:encoded>
    </item>
  </channel>
</rss>