<?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: Ivan Dolgov</title>
    <description>The latest articles on DEV Community by Ivan Dolgov (@aligator527).</description>
    <link>https://dev.to/aligator527</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%2F4203189%2F495dbf84-d2d0-4009-aced-32fa2a0b2eb4.png</url>
      <title>DEV Community: Ivan Dolgov</title>
      <link>https://dev.to/aligator527</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aligator527"/>
    <language>en</language>
    <item>
      <title>A release plan is a return plan</title>
      <dc:creator>Ivan Dolgov</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:05:15 +0000</pubDate>
      <link>https://dev.to/aligator527/a-release-plan-is-a-return-plan-pjj</link>
      <guid>https://dev.to/aligator527/a-release-plan-is-a-return-plan-pjj</guid>
      <description>&lt;p&gt;Every team I have worked with promotes changes through environments, and every team has shipped something that passed all of them and still went wrong in production. That is not a failure of discipline. It is what environments are: a sample of reality, taken under conditions you chose.&lt;/p&gt;

&lt;h2&gt;
  
  
  00 / What staging actually proves
&lt;/h2&gt;

&lt;p&gt;Staging proves that a change does not fail in the ways you thought to look for. It is a real and useful thing to prove, and it is smaller than it feels, because staging is missing the parts of production that cause the interesting failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the data volume, and the one tenant whose data is shaped unlike everyone else's;&lt;/li&gt;
&lt;li&gt;concurrency — real users doing things at the same time, with caches already warm;&lt;/li&gt;
&lt;li&gt;the integrations, in the state they are actually in today;&lt;/li&gt;
&lt;li&gt;the clients, in the versions people are actually running.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the question that decides how a release goes is not "did it pass staging?" but "if it is wrong in production, what do we do in the next ten minutes?" On a live product that question has to have an answer before the deploy, because afterwards you will be answering it under time pressure, in&lt;br&gt;
front of customers, at the hour you chose to release.&lt;/p&gt;

&lt;h2&gt;
  
  
  01 / Three classes of change, and only one is cheap to undo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Reversible by redeploying.&lt;/strong&gt; Code only, no schema change, no data written in a new shape. Going back is deploying the previous artefact — which means keeping it, and having gone back at least once on purpose, so the path is known rather than theoretical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reversible with work.&lt;/strong&gt; A schema or data change made additively: add the new field, write both, backfill, read the new one, and only later stop writing the old. Each step is individually reversible, which is the whole point of doing it in that order. Write down which step is the point&lt;br&gt;
of no return, and treat crossing it as a separate decision with its own date.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not reversible.&lt;/strong&gt; A destructive migration, an email that went out, a call made to someone else's API, a mobile build already on people's phones. For these, "roll back" is not a plan and never was. What works instead is making the change invisible until it is verified: behind a flag, in a shadow table, in a job that writes somewhere nobody reads yet.&lt;/p&gt;

&lt;p&gt;Most incidents I have seen in this area come from a change in the second class being treated as if it were in the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  02 / Blast radius, before the change rather than during it
&lt;/h2&gt;

&lt;p&gt;On a packaging-industry SaaS I worked on, the frontend, the backend and the mobile app lived in separate repositories, each with its own deployment pipeline and its own environments. That is a good structure: each part ships on its own schedule. It also hides something, which I learned while&lt;br&gt;
verifying rather than while planning — they all meet at one data layer, so a change to the shared schema reaches further than the repository you have open.&lt;/p&gt;

&lt;p&gt;What that means in practice: before a change, answer "what else reads this?" by looking, not by remembering. A field you are removing from a GraphQL schema is being selected by clients you did not deploy. The web client can be fixed in an afternoon. The mobile one cannot, because the version in someone's pocket is the version they have until they update, and the slowest updater sets how long your backend keeps serving the old shape.&lt;/p&gt;

&lt;p&gt;The architectural rule that falls out of this is boring and worth following: the shared layer changes additively, and the removal happens one release later than you want it to.&lt;/p&gt;

&lt;h2&gt;
  
  
  03 / Night releases buy a window, not safety
&lt;/h2&gt;

&lt;p&gt;If your customers are concentrated in one timezone, releasing outside their working hours is a cheap and real advantage, and it is not available to everyone. But it is worth being precise about what it buys: not a lower chance of being wrong, only time to notice and act before anyone is&lt;br&gt;
affected.&lt;/p&gt;

&lt;p&gt;That time is only useful if someone is using it. Which means deciding, before the release, what you will look at in the first fifteen minutes — the one query, the one dashboard, the one screen that tells you the migration did what it was supposed to — and who is making the call.&lt;/p&gt;

&lt;h2&gt;
  
  
  04 / The paragraph that does the work
&lt;/h2&gt;

&lt;p&gt;Not a process document. Five lines per change, written while you still have the whole picture in your head:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What changes.&lt;/li&gt;
&lt;li&gt;What else touches it.&lt;/li&gt;
&lt;li&gt;How we check, in production, that it worked.&lt;/li&gt;
&lt;li&gt;How we go back.&lt;/li&gt;
&lt;li&gt;When we stop trying to fix forward and go back instead.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fifth line is the one that matters and the one usually missing. At two in the morning, with a customer's data in an unknown state, nobody decides well how long to keep trying. Deciding it in advance, when it costs nothing, is the whole trick.&lt;/p&gt;

&lt;p&gt;One concrete version of line 3, from a Node.js 16 to 20 migration on that same product: the check was not "the tests pass". It was a defined workflow — set up a tenant, then do the ordinary things a customer does — run by hand against the deployed system, because the dependency and runtime problems that migration produced were not the kind that unit tests notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  05 / What I would settle before the next one
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Which class the change is in, in writing, and where its point of no return is.&lt;/li&gt;
&lt;li&gt;What the previous artefact is and whether anyone has ever deployed it on purpose.&lt;/li&gt;
&lt;li&gt;The oldest client version still in use, and whether the change is safe for it.&lt;/li&gt;
&lt;li&gt;The one check that proves the change worked, and the one that proves it did not.&lt;/li&gt;
&lt;li&gt;Who is awake, and what they are allowed to decide alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The longer version of the engagement these come from — what was diagnosed, what was migrated and how it shipped — is written up as a &lt;a href="https://aligator527.github.io/work/packaging-saas/" rel="noopener noreferrer"&gt;case study&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>database</category>
      <category>architecture</category>
      <category>aws</category>
    </item>
    <item>
      <title>When per-user identity pricing stops working</title>
      <dc:creator>Ivan Dolgov</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:05:09 +0000</pubDate>
      <link>https://dev.to/aligator527/when-per-user-identity-pricing-stops-working-1158</link>
      <guid>https://dev.to/aligator527/when-per-user-identity-pricing-stops-working-1158</guid>
      <description>&lt;p&gt;Identity is the component that is cheapest to adopt and most expensive to leave. The decision is usually made in an afternoon, early, by whoever is setting up the first environment — which is exactly the moment when the per-user option costs nothing, because there are no users.&lt;/p&gt;

&lt;h2&gt;
  
  
  00 / Two different shapes of cost
&lt;/h2&gt;

&lt;p&gt;A managed identity provider bills per monthly active user. A self-hosted one bills per instance, per database, and per engineer. One of those lines slopes with your success. The other is nearly flat, and then steps.&lt;/p&gt;

&lt;p&gt;For most B2B products the sloping line never gets steep: a few thousand seats is a rounding error next to the cost of running an auth server yourself, and the decision is not close. For a consumer product planned to be large, the lines cross. The only questions are where, and whether being wrong is survivable.&lt;/p&gt;

&lt;h2&gt;
  
  
  01 / Price it on the day, not from memory
&lt;/h2&gt;

&lt;p&gt;Rates change, and so do the models. What is worth knowing in advance is the shape of the traps. &lt;a href="https://aws.amazon.com/cognito/pricing/" rel="noopener noreferrer"&gt;Amazon Cognito's pricing page&lt;/a&gt;, read on 2026-10-23:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;three feature tiers, and a new user pool defaults to the middle one — Essentials, at &lt;strong&gt;$0.015 per MAU&lt;/strong&gt; above the free tier;&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;10,000 MAU&lt;/strong&gt; monthly free tier applies on Lite and Essentials. The Plus tier has no free tier at all;&lt;/li&gt;
&lt;li&gt;federated SAML/OIDC users have a separate allowance of &lt;strong&gt;50 MAU per month&lt;/strong&gt;, whatever tier the pool is on;&lt;/li&gt;
&lt;li&gt;user pools created on or before 22 November 2024 keep an older, larger allowance of &lt;strong&gt;50,000 MAU&lt;/strong&gt; — so the price depends not only on which tier you are on but on when the resource was created.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://auth0.com/pricing" rel="noopener noreferrer"&gt;Auth0&lt;/a&gt; prices its B2C plans by monthly active users too, with a free tier in the tens of thousands of MAU at the time of writing. The number that matters for a product planned at scale is the one&lt;br&gt;
not on the page: the top plan is quoted, not listed. If the plan is to grow past the published tiers, the cost of the managed option above that point is not something you can look up — it is something you will negotiate, later, from a position where moving is expensive.&lt;/p&gt;

&lt;p&gt;That asymmetry, not the per-MAU rate, is the thing to notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  02 / What is actually in the self-hosted column
&lt;/h2&gt;

&lt;p&gt;An estimate that sets a per-MAU rate against "an instance and a database" is not an estimate. It is an argument with the inconvenient half left out. The honest column contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Availability.&lt;/strong&gt; Sign-in is on the critical path of everything, including sessions that expire mid-use. An identity server without a failover plan is a single point of failure introduced into a system that may not otherwise have one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upgrades on somebody else's schedule.&lt;/strong&gt; A CVE in an auth server is not a backlog item.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Key rotation, session invalidation, and a tested procedure for "revoke everything, now".&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backups of a store holding password hashes&lt;/strong&gt; — restored at least once, by someone who wrote down how long it took.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account recovery and email deliverability&lt;/strong&gt;, which is the unglamorous half of consumer identity and the half users actually meet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Abuse&lt;/strong&gt;: bot signups, credential stuffing, rate limits per account and per address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The engineer who knows how it works&lt;/strong&gt; — and the second one, for when the first is away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Priced at a loaded hourly rate, that column is rarely small. It is also mostly fixed: the same work serves ten thousand users and ten million. That is precisely why the two lines cross at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  03 / The lock-in is not the protocol
&lt;/h2&gt;

&lt;p&gt;"We can move later, it is all OIDC" is half true. The protocol is portable. The user store is not.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Subject identifiers change.&lt;/strong&gt; Anything that stored &lt;code&gt;sub&lt;/code&gt; as the user's identity has to be remapped, including rows in your own database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password hashes may not be exportable.&lt;/strong&gt; Where a provider will not hand them over, migration means every user resets their password. That is a product event, not a deployment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MFA enrolments, social-account links and refresh tokens do not transfer.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything that trusts the issuer is pinned to it&lt;/strong&gt;: gateway authorizers, mobile builds already in the field, partner integrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the question is not only which option is cheaper but which direction is cheaper to be wrong in.&lt;br&gt;
Leaving a self-hosted provider is a migration you can plan, because you hold the hashes. Leaving a managed one may not be. That asymmetry belongs in the decision and is usually left out of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  04 / Sometimes the argument is not money
&lt;/h2&gt;

&lt;p&gt;Where sign-in belongs is an architecture question before it is a pricing question. If one product signs users in, identity can live inside it. If several products are meant to share one account, identity is a platform component: it belongs outside any single application, on its own hostname,&lt;br&gt;
with its own lifecycle. Moving it there later means changing every redirect URI and every token issuer already shipped.&lt;/p&gt;

&lt;p&gt;On the consumer AI chat product behind &lt;a href="https://aligator527.github.io/work/ai-chat-platform/" rel="noopener noreferrer"&gt;this case study&lt;/a&gt;, that is where it ran: Keycloak on its own domain, intended as the single sign-in entry point for a planned family of AI-interactive applications sharing one database. Cognito and Auth0 both price by user, the product was planned for a large user base, and the per-user line was the one that would have grown with it. During that engagement the wider platform remained design intent rather than a delivered system — which is part of the lesson too. The platform argument is a bet on a roadmap, and roadmaps move.&lt;/p&gt;

&lt;h2&gt;
  
  
  05 / What to settle before committing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Three user-count scenarios — launch, plan, and the number that would count as success — priced on both sides, on the day, not from a blog post.&lt;/li&gt;
&lt;li&gt;Engineer-hours in the self-hosted column, at a loaded rate, including the on-call half.&lt;/li&gt;
&lt;li&gt;The export path for users, hashes and MFA enrolments, written down before launch and confirmed with the provider rather than assumed.&lt;/li&gt;
&lt;li&gt;What happens while identity is down, and whether anyone has tried it.&lt;/li&gt;
&lt;li&gt;Who upgrades the thing, how often, and in what window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either answer is defensible. "It is cheaper" without the second column, and "we can migrate later" without the export path, are not.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>security</category>
      <category>aws</category>
    </item>
    <item>
      <title>Why your DynamoDB app got slow as data grew</title>
      <dc:creator>Ivan Dolgov</dc:creator>
      <pubDate>Fri, 23 Oct 2026 17:59:25 +0000</pubDate>
      <link>https://dev.to/aligator527/why-your-dynamodb-app-got-slow-as-data-grew-51ig</link>
      <guid>https://dev.to/aligator527/why-your-dynamodb-app-got-slow-as-data-grew-51ig</guid>
      <description>&lt;p&gt;The complaint usually arrives in the same shape. The product was fine. Nobody changed the architecture. And now a list screen takes nine seconds, the mobile app times out on export, and it is getting worse every month — for everyone, not for one customer on one screen.&lt;/p&gt;

&lt;p&gt;Nothing broke. That is the part worth sitting with, because it decides where you look.&lt;/p&gt;

&lt;h2&gt;
  
  
  00 / Growth does not break a data model. It reveals one.
&lt;/h2&gt;

&lt;p&gt;A data model encodes the assumptions of the weeks it was written in: how many records there would be, how many tenants, which screens would read what. Those assumptions are rarely written down, and they are never wrong on day one — there is not enough data for them to be wrong yet.&lt;/p&gt;

&lt;p&gt;What changes is the volume, and volume turns a constant-time mistake into a linear one. A full table read costs nothing at a thousand items. At a hundred thousand it is the product.&lt;/p&gt;

&lt;p&gt;So the question is not "what regressed?" Nothing regressed. The question is "which assumption just stopped holding, and where is it encoded?"&lt;/p&gt;

&lt;h2&gt;
  
  
  01 / Read the evidence, not the documentation
&lt;/h2&gt;

&lt;p&gt;On the engagement where I did this at length — a production SaaS with web and mobile clients — the documentation of the data model was out of date, and the team did not share an accurate picture of how it actually behaved. That is normal. Documentation describes the model somebody intended; the&lt;br&gt;
code and the running system describe the one you have.&lt;/p&gt;

&lt;p&gt;Three sources, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The code.&lt;/strong&gt; Every read path. Not what the ORM or the resolver is named — what it actually issues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The queries.&lt;/strong&gt; Profiled, with real parameters, against real volumes. A query that is fast against your seed data tells you nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch.&lt;/strong&gt; Request behaviour over time, so you can tell a slow operation from a slow &lt;em&gt;pattern&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Where the code was hard to follow, I drew the data flow out as a diagram before changing anything. That is not ceremony. If you cannot draw it, you do not know what a change will touch, and on a live product that is the whole risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  02 / The five patterns
&lt;/h2&gt;

&lt;p&gt;These are the ones I have found most often together. Each is individually survivable. They compound.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scan where a Query against a key would do
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Scan&lt;/code&gt; reads the entire table and then filters. &lt;code&gt;Query&lt;/code&gt; goes to a key and reads what it needs. The filter in a &lt;code&gt;Scan&lt;/code&gt; runs &lt;em&gt;after&lt;/em&gt; the read, so a filtered &lt;code&gt;Scan&lt;/code&gt; that returns three items has still read everything you are paying for, in time and in capacity.&lt;/p&gt;

&lt;p&gt;How to confirm it: search the code for &lt;code&gt;Scan&lt;/code&gt; and, for each one, write down which key you would query by instead. If you cannot name one, you have found a key design problem, not a query problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Missing global secondary indexes
&lt;/h3&gt;

&lt;p&gt;A GSI exists so that a common access pattern has a path that is not the primary key. Without it, the only ways to answer "all orders for this customer, newest first" are a &lt;code&gt;Scan&lt;/code&gt; or a read of more items than you need.&lt;/p&gt;

&lt;p&gt;How to confirm it: list the questions your screens actually ask. Each one should map to a key or an index. The ones that map to neither are your list.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keys that do not match how the data is read
&lt;/h3&gt;

&lt;p&gt;This is the one that is actually a redesign, and it is why the first two keep coming back. A partition key chosen because it was the obvious identifier, rather than because it is what reads group by, forces every reader to work around it forever.&lt;/p&gt;

&lt;p&gt;How to confirm it: for each access pattern, ask whether the items you want are adjacent. If the answer is "they are scattered and we filter", the key is wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  N+1 in resolvers
&lt;/h3&gt;

&lt;p&gt;In AppSync — and in any GraphQL layer — a list field resolves once, and then a nested field resolves once &lt;em&gt;per item&lt;/em&gt;. One request for a list of 50 becomes 51 requests. The code looks innocent, because&lt;br&gt;
each resolver is correct in isolation.&lt;/p&gt;

&lt;p&gt;How to confirm it: count the requests for one list screen, not the time. The number should not grow with the size of the list.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full datasets to the client, no pagination
&lt;/h3&gt;

&lt;p&gt;The screen shows twenty rows; the request returns everything the table has. This is the easiest to find and the easiest to fix, which is exactly why it survives for years: it never looks like the problem, because it is not slow until it is.&lt;/p&gt;

&lt;p&gt;How to confirm it: look at response sizes, not durations.&lt;/p&gt;

&lt;h2&gt;
  
  
  03 / Changing it on a product that is live
&lt;/h2&gt;

&lt;p&gt;Diagnosis is the smaller half. The data belongs to customers who are using it right now.&lt;/p&gt;

&lt;p&gt;What I would do again:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ship in milestones&lt;/strong&gt;, each one verified step by step on staging while the team reviews the code in parallel. Not one large change at the end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release outside usage hours&lt;/strong&gt; when the customers are concentrated in one timezone. This is a cheap advantage and it is not available to everyone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update the data-model documentation when you are done.&lt;/strong&gt; You just spent weeks reconstructing it. Leaving it unwritten guarantees the next person repeats that cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I would do differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plan the way back for every change, before making it.&lt;/strong&gt; Promoting through dev, staging and production reduces risk; it does not guarantee that a production release succeeds. A data migration is exactly where that gap hurts, and "we will roll forward" is not a plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map the blast radius first.&lt;/strong&gt; With separately deployed clients sharing one data layer, a change to the shared schema reaches further than the repository you are editing. I learned that while verifying, not while planning.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  04 / Three things I would not do
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A full single-table redesign as the first move.&lt;/strong&gt; It is often the right end state and almost never the right first step: it is the largest possible change to the thing you least understand, made while the symptom is still unexplained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching in front of a Scan.&lt;/strong&gt; This makes the graph look better and leaves the cost exactly where it was, now with a staleness problem on top. Cache after the access pattern is right, not instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chasing a number before defining the measurement.&lt;/strong&gt; Decide what you are measuring, on which operation, at what data volume — before you touch anything. Otherwise you cannot tell improvement from noise, and you will not be able to answer the only question your client will ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  05 / What "better" looked like
&lt;/h2&gt;

&lt;p&gt;I am not going to give you a percentage. No measurement of the before and after on that engagement is approved for publication, and a number I cannot source is worth less than the method.&lt;/p&gt;

&lt;p&gt;What is defensible: after the work, search timeouts decreased and customer complaints decreased substantially. The product kept running throughout.&lt;/p&gt;

&lt;p&gt;If you want the longer version of this specific engagement — the constraints, what was migrated, and how it shipped — it is written up as a &lt;a href="https://aligator527.github.io/work/packaging-saas/" rel="noopener noreferrer"&gt;case study&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>dynamodb</category>
      <category>performance</category>
    </item>
    <item>
      <title>Choosing a stack by proof of concept</title>
      <dc:creator>Ivan Dolgov</dc:creator>
      <pubDate>Fri, 23 Oct 2026 17:59:24 +0000</pubDate>
      <link>https://dev.to/aligator527/choosing-a-stack-by-proof-of-concept-7jb</link>
      <guid>https://dev.to/aligator527/choosing-a-stack-by-proof-of-concept-7jb</guid>
      <description>&lt;p&gt;A technology choice defended with "it is what we know" is sometimes right and never checkable. A proof of concept makes it checkable — but only when it is run as a decision procedure, and not as an early version of the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  00 / A proof of concept is a question with a deadline
&lt;/h2&gt;

&lt;p&gt;Three things separate it from a prototype: a question whose answer is allowed to be no, a criterion agreed before anyone writes code, and a date on which it ends. Drop any one of them and the PoC quietly becomes the first version of the system — the demo ships, nobody re-reads the question, and the stack has been chosen by sunk cost.&lt;/p&gt;

&lt;p&gt;The question has to be able to fail. "Can we build this in X?" cannot: the answer is always yes, given enough time. "What does X cost us on the part we are actually worried about, in two days?" can.&lt;/p&gt;

&lt;h2&gt;
  
  
  01 / Make the comparisons comparable
&lt;/h2&gt;

&lt;p&gt;Same task, same timebox, same reviewer, and the criteria with their weights written down before the first commit. The task should be the thing you are afraid of — the query that worries you, the concurrency, the integration whose documentation looks thin — and not an endpoint every candidate&lt;br&gt;
handles identically.&lt;/p&gt;

&lt;p&gt;Three ways this goes wrong, all of them common:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;each candidate is built by the person who already prefers it;&lt;/li&gt;
&lt;li&gt;the candidates get different amounts of time, usually in favour of whoever started first;&lt;/li&gt;
&lt;li&gt;the criteria are written after the results, and fit the winner remarkably well.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  02 / Criteria that survive the meeting
&lt;/h2&gt;

&lt;p&gt;On the consumer product behind &lt;a href="https://aligator527.github.io/work/ai-chat-platform/" rel="noopener noreferrer"&gt;this case study&lt;/a&gt;, the backend language was chosen after several proofs of concept rather than by preference, against four criteria: scalability, maintainability, speed, and how well the codebase would work with AI-assisted development. Go came out of that balance, and other parts of the stack were decided the&lt;br&gt;
same way.&lt;/p&gt;

&lt;p&gt;Three of those four are ordinary, and each is worth turning into a question with an answer rather than an adjective:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt; — what happens at ten and a hundred times the load you expect, and what does it cost there? Measure it; do not reason from the vendor's page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintainability&lt;/strong&gt; — how long does it take someone who did not write the code to make a small change safely? That is a number, and the PoC is the cheapest time to take it, while the codebase is still small enough for a newcomer to read in an hour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt; — of the system and of the team. A runtime that is fast and that nobody on the team writes fluently is slow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  03 / The criterion almost nobody tests for
&lt;/h2&gt;

&lt;p&gt;The fourth criterion is the one worth being explicit about, because teams now take it on faith in one direction or the other: how well a codebase in this stack will hold up when much of it is written with an assistant.&lt;/p&gt;

&lt;p&gt;It is testable, and the thing to measure is the review, not the output. Give the same feature to the assistant in each candidate and look at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how much of what came back was correct, and how much was plausible and wrong;&lt;/li&gt;
&lt;li&gt;what the compiler or the type system rejected before a human had to — a stack where wrong code fails to build is cheaper than one where it runs and fails later;&lt;/li&gt;
&lt;li&gt;whether the standard library covered what the assistant would otherwise have pulled a dependency for, because dependencies chosen by autocomplete are a maintenance decision made by nobody;&lt;/li&gt;
&lt;li&gt;whether formatting is settled by a tool, so that diffs are about logic instead of style;&lt;/li&gt;
&lt;li&gt;how long the result took to review, which is the number that actually scales with the team.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A large public corpus helps. It is not the whole story: the wider a language's surface, the more there is to be confidently wrong about, and the more a reviewer has to hold in their head to catch it.&lt;/p&gt;

&lt;h2&gt;
  
  
  04 / Letting the answer be no
&lt;/h2&gt;

&lt;p&gt;A PoC can also answer "do we keep what is already here?". On that engagement an earlier codebase existed, written by one developer more than a year earlier, and for the scope being started it offered little foundation. That is a legitimate result, and it needs the same written question and the same deadline as any other — otherwise the discussion drifts towards the previous author instead of the code.&lt;/p&gt;

&lt;p&gt;When a question has been answered, the PoC should be deleted, or promoted deliberately with a plan for the parts that were never finished. Kept "just as a reference", it gets copied.&lt;/p&gt;

&lt;h2&gt;
  
  
  05 / The half a PoC does not cover
&lt;/h2&gt;

&lt;p&gt;The language decision on that project anticipated AI-assisted development. The team's workflow for it was not standardised from the start, and output often did not meet shared expectations and had to be reworked, which lowered quality overall. It is the thing I would do differently, and it is&lt;br&gt;
not a tooling problem.&lt;/p&gt;

&lt;p&gt;A technology evaluation answers &lt;em&gt;which tool&lt;/em&gt;. It does not answer &lt;em&gt;how we work&lt;/em&gt;: what a task looks like before it is handed to an assistant, what review covers that it did not before, what gets written by hand, and what nobody merges without reading twice. That second question deserves the&lt;br&gt;
same deliberate treatment as the first, and usually gets none — because it has no artefact to show at the end of the week.&lt;/p&gt;

&lt;p&gt;Choose with evidence, then write down how the chosen thing is used. The first half is the easy one.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>go</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
