<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aleksander Frolov</title>
    <description>The latest articles on DEV Community by Aleksander Frolov (@aleksander_frolov).</description>
    <link>https://dev.to/aleksander_frolov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4079810%2Fa40a6439-379c-4aee-a94a-5a9f6a7de24c.jpg</url>
      <title>DEV Community: Aleksander Frolov</title>
      <link>https://dev.to/aleksander_frolov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aleksander_frolov"/>
    <language>en</language>
    <item>
      <title>A modern API reference for Symfony with Scalar</title>
      <dc:creator>Aleksander Frolov</dc:creator>
      <pubDate>Sun, 30 Aug 2026 07:21:55 +0000</pubDate>
      <link>https://dev.to/aleksander_frolov/a-modern-api-reference-for-symfony-with-scalar-1dh5</link>
      <guid>https://dev.to/aleksander_frolov/a-modern-api-reference-for-symfony-with-scalar-1dh5</guid>
      <description>&lt;p&gt;Symfony had no official integration with Scalar — the open-source API Reference renderer that makes Swagger UI look its age. Laravel got one back in 2024: &lt;code&gt;scalar/laravel&lt;/code&gt; has &lt;strong&gt;231,862 installs&lt;/strong&gt; on Packagist. Symfony got nothing — not a single package on Packagist, and zero in Scalar's official list of 30+ integrations (Express, FastAPI, NestJS, Spring Boot, Laravel — but no Symfony).&lt;/p&gt;

&lt;p&gt;I work with Symfony daily, so I closed the gap: the &lt;code&gt;alex-frolov/scalar-symfony&lt;/code&gt; bundle.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the bundle does
&lt;/h2&gt;

&lt;p&gt;It renders Scalar API Reference from &lt;strong&gt;any&lt;/strong&gt; OpenAPI document. One route, zero coupling to how the spec was generated: a static &lt;code&gt;openapi.yaml&lt;/code&gt;, swagger-php, NelmioApiDocBundle, or API Platform all work — because the bundle never parses or proxies the document; the page loads it client-side.&lt;/p&gt;

&lt;p&gt;Install and configure in two files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require alex-frolov/scalar-symfony
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/packages/scalar_symfony.yaml&lt;/span&gt;
&lt;span class="na"&gt;scalar_symfony&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/openapi.yaml'&lt;/span&gt;          &lt;span class="c1"&gt;# your OpenAPI document (required)&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/scalar'&lt;/span&gt;               &lt;span class="c1"&gt;# route (default: /scalar)&lt;/span&gt;
    &lt;span class="na"&gt;cdn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.65.1'&lt;/span&gt;

    &lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;default'&lt;/span&gt;
        &lt;span class="na"&gt;metaData&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;API&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Reference'&lt;/span&gt;

    &lt;span class="na"&gt;scalar_options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;               &lt;span class="c1"&gt;# any Scalar option, passed through as is&lt;/span&gt;
        &lt;span class="na"&gt;darkMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;layout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;modern'&lt;/span&gt;

    &lt;span class="na"&gt;access_control&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public&lt;/span&gt;              &lt;span class="c1"&gt;# or 'attribute' + security attribute&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it — the reference lives at &lt;code&gt;/scalar&lt;/code&gt;. The config is serialized XSS-safely (JSON_HEX_TAG/APOS/AMP/QUOT), so even a malicious &lt;code&gt;title&lt;/code&gt; can't break out of the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documentation that works as a client
&lt;/h2&gt;

&lt;p&gt;The bundle serves the API reference of the Tender Platform (Symfony 8.1, highload auction API, OpenAPI 3.1 spec). Through Test Request I'm hitting &lt;code&gt;POST /auth/register&lt;/code&gt;, filling the JSON body, and pressing Send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;HTTP/&lt;/span&gt;&lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Created&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;794&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ms)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"company_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0c7702c6-9667-4ea3-8caa-df4990522ee7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"86a40d70-5ef9-44fd-882b-8f703e10df7e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verification_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real UUIDs, real latency, real backend: the docs page doubles as the tool you test the API with. That's what Swagger UI never delivered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quality bar
&lt;/h2&gt;

&lt;p&gt;The first commit was small; the release survived a review against Symfony's official bundle best practices and external hardening passes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Functional tests:&lt;/strong&gt; 16 tests / 46 assertions — routes 200/403/404, config validation errors, XSS-escaping, real SecurityBundle integration;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static analysis:&lt;/strong&gt; PHPStan level max, 0 errors;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI matrix:&lt;/strong&gt; PHP 8.2/8.3/8.5 × Symfony 6.4/7.2/7.4/8.0, including &lt;code&gt;--prefer-lowest&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config hardening:&lt;/strong&gt; &lt;code&gt;attribute&lt;/code&gt; mode without Symfony Security fails &lt;code&gt;cache:clear&lt;/code&gt; at compile time with a clear message;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security docs:&lt;/strong&gt; SRI (SHA-384), CSP/nonce guidance, self-hosting recipe.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The CI war story: a token that didn't fit
&lt;/h2&gt;

&lt;p&gt;Half the GitHub Actions jobs failed with &lt;code&gt;Your github oauth token for github.com contains invalid characters&lt;/code&gt;. &lt;code&gt;setup-php&lt;/code&gt; writes the Actions &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; (prefixed &lt;code&gt;ghs_&lt;/code&gt;) into composer's global &lt;code&gt;auth.json&lt;/code&gt;, and Composer 2.8 only accepts &lt;code&gt;ghp_&lt;/code&gt;/&lt;code&gt;gho_&lt;/code&gt;/&lt;code&gt;github_pat_&lt;/code&gt;. The fix: delete &lt;code&gt;auth.json&lt;/code&gt; on the runner before &lt;code&gt;composer validate --no-check-publish&lt;/code&gt;, keep the token for dependency installation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it official
&lt;/h2&gt;

&lt;p&gt;I opened a proposal in the Scalar organization — &lt;strong&gt;Discussion #9920: "Proposal: official Symfony integration (scalar/symfony)"&lt;/strong&gt; — &lt;a href="https://github.com/scalar/scalar/discussions/9920" rel="noopener noreferrer"&gt;https://github.com/scalar/scalar/discussions/9920&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're a Symfony developer who wants modern API docs the way Laravel has them, a reaction on the discussion helps signal maintainer attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start tomorrow:&lt;/strong&gt; &lt;code&gt;composer require alex-frolov/scalar-symfony&lt;/code&gt; → point &lt;code&gt;scalar_symfony.url&lt;/code&gt; at any OpenAPI document → import routes and open &lt;code&gt;/scalar&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Aleksander Frolov — Senior/Staff PHP engineer, Symfony/Laravel, highload. &lt;a href="https://frolov.guru" rel="noopener noreferrer"&gt;frolov.guru&lt;/a&gt; · &lt;a href="https://github.com/alex-frolov/scalar-symfony" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>symfony</category>
      <category>architecture</category>
      <category>openapi</category>
    </item>
    <item>
      <title>Idempotency and Retry in a Payment Core: Operations That Can't Be Duplicated and Can't Be Forgotten</title>
      <dc:creator>Aleksander Frolov</dc:creator>
      <pubDate>Sun, 16 Aug 2026 09:16:29 +0000</pubDate>
      <link>https://dev.to/aleksander_frolov/idempotency-and-retry-in-a-payment-core-operations-that-cant-be-duplicated-and-cant-be-forgotten-5ff7</link>
      <guid>https://dev.to/aleksander_frolov/idempotency-and-retry-in-a-payment-core-operations-that-cant-be-duplicated-and-cant-be-forgotten-5ff7</guid>
      <description>&lt;h1&gt;
  
  
  Idempotency and Retry in a Payment Core: Operations That Can't Be Duplicated and Can't Be Forgotten
&lt;/h1&gt;

&lt;p&gt;Build operations that can't be duplicated and can't be forgotten — that's the contract for a payment core.&lt;/p&gt;

&lt;p&gt;On the core I built for ROSSTRAFFY (20,000+ successful transactions/day, 99.99% uptime), the most dangerous failure class wasn't crashes. It was retries.&lt;/p&gt;

&lt;p&gt;The network doesn't guarantee delivery. A request can be processed, but the response lost — the client retries, and the server can't tell "didn't arrive" from "processed, but response lost". If a retry means "run the request again", you get double charges. If it means "resume the same operation", you get exactly one outcome per key.&lt;/p&gt;

&lt;p&gt;The contract: every payment happens exactly once — not 0, not 2 times. Zero means a lost payment and a missed discount window; two means double charges, refunds, disputes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The approach: the payment core as a state machine
&lt;/h2&gt;

&lt;p&gt;A payment operation is not a "request" that either ran or didn't. It's a path with state: created, processing, completed, rejected, in reconciliation. Every transition must be idempotent — applying the same transition twice must not change the result.&lt;/p&gt;

&lt;p&gt;Idempotency lives at three levels, and skipping any one lets duplicates through:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;API level&lt;/strong&gt; — one idempotency key per business operation, generated once, surviving all retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handler level&lt;/strong&gt; — the operation is a state machine; every transition checks the current state before acting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage level&lt;/strong&gt; — unique constraints as the last line of defense.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp45vdvnrxd166rexpj84.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp45vdvnrxd166rexpj84.png" alt="Operation state machine: every transition is idempotent" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What we built
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operation = state machine&lt;/strong&gt;: every transition idempotent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One idempotency key per business operation&lt;/strong&gt;, generated once, surviving all retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency at three levels&lt;/strong&gt;: API, handler, storage (unique constraints as the last line of defense).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment = two operations&lt;/strong&gt;: hold (block funds) → capture (charge after the external system confirms), each with its own key and retry rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync leg&lt;/strong&gt;: bounded retries, exponential backoff, timeouts, jitter, dead-letter queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async leg (ГИС ГМП)&lt;/strong&gt;: queue + workers, load balancing across 5+ acquirers (Strategy pattern), failover on degradation → 99.8% acceptance reliability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recurring payments stay on the same acquirer&lt;/strong&gt; (confirmed token); switching only for new payments and emergencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt;: counters + latency per stage in Grafana, alerts on anomalies. "More retries, fewer successful operations" = retry is masking the problem, not fixing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciliations as the second safety net&lt;/strong&gt; (ClickHouse: daily financial reports in 3–5 minutes instead of hours).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fup22vcxn1kfiqqaf0cu3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fup22vcxn1kfiqqaf0cu3.png" alt="Hold and capture: exactly-once contract" width="799" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyy535uy1lyhr1wjhrcnt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyy535uy1lyhr1wjhrcnt.png" alt="Async leg: queue, balancing, payment inspector, reconciliations" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation and results
&lt;/h2&gt;

&lt;p&gt;We validated with A/B tests behind feature flags on real traffic. Effects were fractions of a percent of conversion — only real payment statistics could see them; synthetic tests couldn't.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Erroneous and fraudulent operations&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−25%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment conversion&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+~10%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median transaction processing time&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−15%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;Idempotency isn't a nice-to-have in payments. It's the contract that makes retries safe at scale — and what turns "the user paid twice" into "the user paid once, and we can prove it".&lt;/p&gt;

&lt;p&gt;The full article with diagrams and code (EN): &lt;a href="https://frolov.guru/en/writing/idempotency-retry/" rel="noopener noreferrer"&gt;https://frolov.guru/en/writing/idempotency-retry/&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;About the author: Alexander Frolov — Senior/Staff Backend Engineer (PHP, highload) and Team Lead, 18+ years: payment cores, multi-tenant platforms at federal scale. Articles on highload PHP and architecture: &lt;a href="https://frolov.guru" rel="noopener noreferrer"&gt;frolov.guru&lt;/a&gt;. If you're fighting timeouts, duplicates or lost payments — that's an idempotency problem, and it's fixable. DM or &lt;a href="mailto:aleksander@frolov.guru"&gt;aleksander@frolov.guru&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>performance</category>
      <category>php</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
