<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>karloscodes</title><description>Thoughts on building software that lasts.</description><link>https://karloscodes.com/</link><item><title>Logs are enough</title><link>https://karloscodes.com/blog/logs-are-enough/</link><guid isPermaLink="true">https://karloscodes.com/blog/logs-are-enough/</guid><description>Coinbase paid $65 million a year to watch its systems. You need logs, a ping, and a disk alert.</description><pubDate>Wed, 08 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In 2021, Coinbase paid Datadog around &lt;a href=&quot;https://blog.pragmaticengineer.com/datadog-65m-year-customer-mystery/&quot;&gt;$65 million&lt;/a&gt;. Not for infrastructure. For watching it. The number surfaced on Datadog&amp;#39;s own earnings call, and the industry&amp;#39;s reaction was telling: not &amp;quot;how absurd,&amp;quot; but &amp;quot;how do we get a customer like that.&amp;quot;&lt;/p&gt;
&lt;p&gt;That&amp;#39;s the observability business. It sells three pillars, metrics, traces, and logs, each with its own pricing meter. And somewhere along the way, the three pillars stopped being a product tier and became a definition of professionalism. Teams of four now feel negligent without distributed tracing.&lt;/p&gt;
&lt;p&gt;Look at what each pillar is actually for. Traces follow a request across dozens of services, because no human can. Metrics compress logs that have become too voluminous to read. Dashboards give forty engineers one shared picture of a system none of them fully knows. All three exist for bigness: tools for understanding systems that outgrew understanding.&lt;/p&gt;
&lt;p&gt;Your system didn&amp;#39;t. In a monolith on one box, the trace is a stack trace with a request ID on it. The latency percentiles are sitting in your request logs&amp;#39; duration field, one query away. Error-rate alerts derive from the same lines. The shared mental model the dashboard sells? You have it, in your head, because the system fits there. You&amp;#39;d be buying X-ray glasses for a body you can see.&lt;/p&gt;
&lt;p&gt;The title oversells by two small items, so let&amp;#39;s be exact. Dead servers don&amp;#39;t log: you need one external ping that notices you&amp;#39;re gone. And a full disk kills logging first: you need one disk alert, plus logrotate, which has been capping disks since before observability was a word. Keep a week or two of history and compress the rest away. Almost every question you&amp;#39;ll ask your logs is about the last few days, and logs you no longer have are logs nobody can leak.&lt;/p&gt;
&lt;p&gt;Logs, a ping, and a disk alert: the complete tower at your scale, and notice there&amp;#39;s still not a pillar in it. What you&amp;#39;re deleting is the industry, the metric pipelines and trace collectors and per-host pricing, not the last two percent of safety.&lt;/p&gt;
&lt;p&gt;You don&amp;#39;t need three pillars to hold up one box.&lt;/p&gt;
</content:encoded></item><item><title>Five nines for customers who sleep</title><link>https://karloscodes.com/blog/five-nines-for-customers-who-sleep/</link><guid isPermaLink="true">https://karloscodes.com/blog/five-nines-for-customers-who-sleep/</guid><description>The whole war between 99.9% and 99.99% is 39 minutes a month. Your customers are asleep for most of them.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;99.9% uptime allows 43 minutes of downtime a month. 99.99% allows four. The entire argument between those two numbers, the architecture reviews, the multi-region diagrams, the on-call rotations, is a fight over 39 minutes.&lt;/p&gt;
&lt;p&gt;Now price the 39 minutes. The industry&amp;#39;s own rule of thumb says each extra nine costs roughly ten times more: redundant everything, failover that needs rehearsing, a second region you hope stays in sync, a pager that owns somebody&amp;#39;s weekend. The most expensive minutes in engineering, bought monthly, forever.&lt;/p&gt;
&lt;p&gt;Then ask who would have noticed them. If you sell B2B, your customers use the product during their working hours: call it 50 of the week&amp;#39;s 168. They are asleep, commuting, or living their lives for the other 70%. Downtime at 3am on a Sunday costs you nothing but the uptime badge. Most of the 39 minutes you&amp;#39;re insuring against would land in hours when nobody&amp;#39;s there.&lt;/p&gt;
&lt;p&gt;And there&amp;#39;s a ceiling no SLA budget can break: availability multiplies. Your app times your cloud provider times your DNS times your payment processor. You cannot out-nine your dependencies. When a big cloud region has a bad afternoon, half the internet goes down together, and customers blame the internet, not you. Inside the herd, outages are forgiven. The nine you bought above your dependencies was never reachable.&lt;/p&gt;
&lt;p&gt;What reliability looks like at your scale is different and mostly free. An external ping that tells you you&amp;#39;re down before a customer does. A backup you&amp;#39;ve actually restored, with a stopwatch running. A status page that tells the truth, and the email that says what broke and when it will be fixed. Customers don&amp;#39;t remember the 20 minutes you were down. They remember whether you told them.&lt;/p&gt;
&lt;p&gt;Some businesses earn the nines: payments, hospitals, anything with real traffic at 3am. The signal is in your access logs, not in your ambitions. Open them and look at what happens at night.&lt;/p&gt;
&lt;p&gt;Buy the nines your customers are awake for.&lt;/p&gt;
</content:encoded></item><item><title>Names yes, geometry no</title><link>https://karloscodes.com/blog/names-yes-geometry-no/</link><guid isPermaLink="true">https://karloscodes.com/blog/names-yes-geometry-no/</guid><description>You learned the hexagon at a job with hundreds of engineers. Your small project doesn&apos;t need a treaty. Small is a competitive advantage.</description><pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You learned hexagonal architecture at a job with hundreds of engineers, and it made sense there: layers as treaties between teams, interfaces as borders nobody crosses without paperwork. Then you started something small. A side project, a tiny company. And you brought the hexagon with you, because that&amp;#39;s what professional code looks like.&lt;/p&gt;
&lt;p&gt;The skills transferred. The context didn&amp;#39;t.&lt;/p&gt;
&lt;p&gt;Keep the half that costs zero lines of code: the names. Domain-Driven Design got this part right, and it&amp;#39;s the thin part of a thick book. Call things what the business calls them. If the accountant says invoice, the class is Invoice, not BillingDocumentEntity. Draw boundaries as vocabulary: this module talks about shipments, that one about payments, and they don&amp;#39;t borrow each other&amp;#39;s words. That&amp;#39;s thinking, not architecture, and it&amp;#39;s worth the same at any headcount. The rest of the DDD catalog, the aggregates and repositories and factories, is geometry by another name. Leave it in the book.&lt;/p&gt;
&lt;p&gt;Because the geometry&amp;#39;s promise is seductive. Someday you&amp;#39;ll swap Postgres. Someday the big refactor will glide along those segregated interfaces. Someday never comes. At the old job, that was fine: the premium was split across a hundred engineers, and insurance you never claim is what bigness costs. At your scale, the whole premium lands on five of you, or just on you. The DTOs, the mappers, the interfaces, the wiring: a handful of hands maintains all of it, waiting for a someday that was never yours to begin with. Enterprise insurance on a small house.&lt;/p&gt;
&lt;p&gt;The hexagon was a treaty between a hundred hands in one codebase. There&amp;#39;s no one to sign treaties with anymore. Code that fits in one head needs no protection from other heads. Smallness already does the hexagon&amp;#39;s job. Small requires less, and the requiring less is the whole opportunity.&lt;/p&gt;
&lt;p&gt;And staying small is a decision with champions. 37signals has run Basecamp and HEY for millions of users with a few dozen programmers, on one monolith, by choice, for two decades. They didn&amp;#39;t fail to grow. They refused the bigness so they&amp;#39;d never need the treaties.&lt;/p&gt;
&lt;p&gt;The hexagon was sold to big enterprises, in books written for engineering at scale. Nobody wrote the book for your scale. So here&amp;#39;s the short version: names yes, geometry no. Small isn&amp;#39;t a phase to apologize for. It&amp;#39;s a competitive advantage, and skipping the hexagon is how you cash it.&lt;/p&gt;
&lt;p&gt;Keep the language. Lose the geometry.&lt;/p&gt;
</content:encoded></item><item><title>Aggregate at write time</title><link>https://karloscodes.com/blog/aggregate-at-write-time/</link><guid isPermaLink="true">https://karloscodes.com/blog/aggregate-at-write-time/</guid><description>A year of hourly reports is 8,760 rows. You are not Yandex, and their solution is sized for their problem.</description><pubDate>Wed, 17 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;ClickHouse is short for Clickstream Data Warehouse. It was built at Yandex to power Metrica, their web analytics platform, which grew from 200 million events a day to &lt;a href=&quot;https://clickhouse.com/blog/evolution-of-data-structures-in-yandexmetrica&quot;&gt;more than 25 billion by 2016&lt;/a&gt;. They open sourced it, it&amp;#39;s a marvel, and it has become the reflex: need analytics, need reports, store the raw stream and query it later.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s the detail buried in their own history. Metrica ran for years on pre-built aggregates. Yandex moved to querying raw events because, at their scale, the combinations of reports people wanted exploded past what rollups could hold. ClickHouse exists because Yandex outgrew aggregation. The threshold is measured in billions of events a day.&lt;/p&gt;
&lt;p&gt;At that scale, the warehouse makes sense. At your scale, most reports your product produces never need one. The move that replaces it: aggregate at write time. Keep one row per metric per hour, and when an event arrives, bump its counter instead of storing the event.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO hourly_views (hour, views)
VALUES (date_trunc(&amp;#39;hour&amp;#39;, now()), 1)
ON CONFLICT (hour) DO UPDATE SET views = views + 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&amp;#39;s the whole trick. The report isn&amp;#39;t computed when someone opens it. It was built all along, one increment per event, by the same write that recorded it. The raw event is gone, and the answer is already sitting on disk.&lt;/p&gt;
&lt;p&gt;That skips the part the pitch leans on hardest. The warehouse doesn&amp;#39;t just store your data, it promises a generic engine: pipe every raw event in, often through something like Kafka, and query it any way you want later. That power is the real product, and it&amp;#39;s the deeper trap. Generic querying is for when you don&amp;#39;t know what you&amp;#39;ll ask. You do. The reports your product has to produce, a usage summary for you or a stats page for your customers, were designed up front and rarely change. You&amp;#39;re not running a research lab, you&amp;#39;re rendering the same reports on a schedule. Renting a generic engine to answer fixed questions is paying twice, the pipeline that feeds it and the engine that serves it, for a number a pre-computed total hands you with one SELECT.&lt;/p&gt;
&lt;p&gt;So compute the answer once, because you can. You know your questions.&lt;/p&gt;
&lt;p&gt;Now count the rows. Aggregated by day, a year of any report is 365 rows. By hour, 8,760. Track ten reports at hourly grain and a full year is under 90,000 tiny rows, fewer than one busy afternoon of raw events.&lt;/p&gt;
&lt;p&gt;Look at the size of it. A healthy small product does 10,000 tracked events a day. Stored raw, that&amp;#39;s 3.65 million rows a year, most of a gigabyte, growing forever, re-scanned every time a report runs. Aggregated hourly, it&amp;#39;s 8,760 rows. About a megabyte. A thousand times less storage. Four hundred times fewer rows. The same reports, instant. The work happens once, when the event lands, and never again.&lt;/p&gt;
&lt;p&gt;I run my analytics this way: &lt;a href=&quot;https://karloscodes.com/blog/sqlite-for-web-analytics/&quot;&gt;hourly rollups in SQLite&lt;/a&gt;. Six months of production traffic came to &lt;a href=&quot;https://karloscodes.com/blog/small-data/&quot;&gt;14 megabytes&lt;/a&gt;. Every chart is a read of pre-computed rows, instant on a $5 VPS.&lt;/p&gt;
&lt;p&gt;There&amp;#39;s a second prize. Raw events stored forever are pure liability: a bill with a breach radius. Aggregates are small by construction and anonymous by arithmetic. The privacy policy writes itself.&lt;/p&gt;
&lt;p&gt;You are not Yandex. Be glad. They need the generic engine: their questions are unknowable and their data has nine zeros. Yours fit on a phone. So skip the machine they&amp;#39;re stuck running and put the saved time and money into the product.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s the decision, and it isn&amp;#39;t close. Aggregated reports are a competitive advantage, and you get it for being small and refusing to go generic. Simple wins.&lt;/p&gt;
&lt;p&gt;Store answers, not questions.&lt;/p&gt;
</content:encoded></item><item><title>The database is your queue</title><link>https://karloscodes.com/blog/the-database-is-your-queue/</link><guid isPermaLink="true">https://karloscodes.com/blog/the-database-is-your-queue/</guid><description>Kafka moves 7 trillion messages a day at LinkedIn. You have four thousand jobs and a database that&apos;s bored.</description><pubDate>Thu, 11 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Kafka was built at LinkedIn to move activity streams. By the time they wrote it up, it was carrying &lt;a href=&quot;https://www.linkedin.com/blog/engineering/open-source/apache-kafka-trillion-messages&quot;&gt;7 trillion messages a day across 4,000 brokers&lt;/a&gt;. Serious engineering for a serious problem. Their problem.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s the reflex it created. You need background jobs: send the welcome email, retry the webhook, build the nightly report. The tutorial says install Redis, add a queueing library, and if you&amp;#39;re &amp;quot;serious,&amp;quot; RabbitMQ or Kafka. Three systems running before your first job does.&lt;/p&gt;
&lt;p&gt;It&amp;#39;s a trap at your scale. And it&amp;#39;s not your fault: the books were written for Google scale, and nobody told you they weren&amp;#39;t talking to you.&lt;/p&gt;
&lt;p&gt;Now count your jobs. Not the jobs you might have. The ones you have. A normal product runs a few thousand a day, with rush hours that might touch a few per second. You are about to deploy LinkedIn&amp;#39;s artillery against a queue that fits on a napkin.&lt;/p&gt;
&lt;p&gt;The database you already run handles this without noticing. A jobs table and &lt;code&gt;SELECT ... FOR UPDATE SKIP LOCKED&lt;/code&gt; is the whole trick, and every serious framework has it packaged. Rails 8 went further: the &lt;a href=&quot;https://dev.37signals.com/solid-queue-v1-0/&quot;&gt;default queue is now the database&lt;/a&gt;. Not for toy apps. HEY, 37signals&amp;#39; email service, pushes about 20 million jobs a day through it. That&amp;#39;s 230 jobs a second, on the technology the tutorials told you couldn&amp;#39;t handle your fifty.&lt;/p&gt;
&lt;p&gt;Do the math. Four thousand jobs a day is one job every twenty seconds. The database queue, proven at 230 jobs a second, clears your whole day in 17 seconds. LinkedIn&amp;#39;s Kafka would clear your year in two hundredths of a second. One of these is a reference class. The other is a costume.&lt;/p&gt;
&lt;p&gt;And the database queue gives you something the broker never will: one transaction. Save the order and enqueue its email in the same commit, and there is no crack between systems for a job to fall into. With a separate broker there is always a crack, and the industry&amp;#39;s fix for it is the outbox pattern, which is, read it slowly, a jobs table in your database. The cure for the second system is the first one.&lt;/p&gt;
&lt;p&gt;Your jobs become rows. You inspect the queue with a SELECT. You count failures with a WHERE. Your backup backs it up. Nothing new to monitor. One less system that can break.&lt;/p&gt;
&lt;p&gt;And half your queue doesn&amp;#39;t even need to be a queue. The welcome email can leave a minute late. The nightly report was never urgent. This is where cron stops being the boring old tool and becomes the discovery of your architecture: a fifty-year-old scheduler, zero dependencies, that has outlived every framework it ever served. One crontab line sweeping the table every minute is real-time enough for most of what a business does, and it breaks less often than a worker fleet. Batch was never the compromise. It was the right size all along.&lt;/p&gt;
&lt;p&gt;You&amp;#39;re small. At your scale you get to do less. Take that: it&amp;#39;s a competitive advantage. Simple wins, and when you grow, you adapt.&lt;/p&gt;
&lt;p&gt;The queue you&amp;#39;re shopping for is already running. It&amp;#39;s called your database.&lt;/p&gt;
&lt;p&gt;You&amp;#39;re allowed to just use it.&lt;/p&gt;
</content:encoded></item><item><title>Do you know what one server can do?</title><link>https://karloscodes.com/blog/do-you-know-what-one-server-can-do/</link><guid isPermaLink="true">https://karloscodes.com/blog/do-you-know-what-one-server-can-do/</guid><description>A billion rows in 1.5 seconds on 8 cores. The hardware got 100x better. The advice never got re-priced.</description><pubDate>Wed, 10 Jun 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In 2024, Gunnar Morling ran a challenge: aggregate one billion temperature readings from a text file, as fast as you can. The &lt;a href=&quot;https://www.morling.dev/blog/1brc-results-are-in/&quot;&gt;winning solution took 1.5 seconds&lt;/a&gt;. Not on a cluster. On 8 cores of a single rented server.&lt;/p&gt;
&lt;p&gt;One billion rows. 1.5 seconds. One machine.&lt;/p&gt;
&lt;p&gt;I keep meeting engineers who can sketch Google&amp;#39;s architecture from memory but can&amp;#39;t tell me what one modern server does. We memorized their numbers and never measured our own. So: do you know what one server can do?&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s the box. A &lt;a href=&quot;https://www.hetzner.com/dedicated-rootserver/matrix-ax/&quot;&gt;Hetzner AX42&lt;/a&gt; costs about €50 a month. Eight cores, 64 GB of ECC RAM, NVMe storage. The challenge machine was bigger, but results were capped to eight of its cores, five years older than these. The whole machine costs less per month than one hour of the engineer architecting around it.&lt;/p&gt;
&lt;p&gt;64 GB of RAM means the entire customer database of almost every B2B company on earth fits in memory, with room left for ten copies.&lt;/p&gt;
&lt;p&gt;Same story on disk. A million-row table, the kind that gets called big in planning meetings, is about 100 MB with its indexes. Twenty-five phone photos. The billion-row file from Morling&amp;#39;s challenge was around 13 GB. The box holds forty of them on half a terabyte of NVMe. You are not running out of disk. You have never been close.&lt;/p&gt;
&lt;p&gt;NVMe does random reads on the order of a million IOPS. The spinning disk that your architecture patterns were designed around did about two hundred. Two hundred. Every storage pattern you learned was a workaround for that number.&lt;/p&gt;
&lt;p&gt;Throughput is arithmetic. A sustained 1,000 requests per second is 86 million requests a day. A plain Go service does tens of thousands of requests per second on eight cores; Rails, nobody&amp;#39;s idea of fast, does thousands. SQLite takes tens of thousands of inserts per second batched in WAL mode, on the database engine everyone tells you is a toy.&lt;/p&gt;
&lt;p&gt;You will probably never see 86 million requests in a day. Stack Overflow served one of the busiest sites on the internet from &lt;a href=&quot;https://nickcraver.com/blog/2016/02/17/stack-overflow-the-architecture-2016-edition/&quot;&gt;about nine web servers&lt;/a&gt;. That was a decade ago, on weaker hardware than the €50 box.&lt;/p&gt;
&lt;p&gt;Now put a normal business next to those numbers. A B2B product with a thousand daily active users peaks at a few dozen requests per second. Its database, after years of trading, holds a few gigabytes. Its background work is thousands of jobs a day, not millions. A normal business fits in a corner of this machine, and the machine is bored.&lt;/p&gt;
&lt;p&gt;So why do we shard databases that fit in RAM and run queue clusters for jobs one core could clear before lunch?&lt;/p&gt;
&lt;p&gt;Because we&amp;#39;re running someone else&amp;#39;s playbook. The books and talks that taught us architecture were written by Googlers and Amazoners, solving Google and Amazon problems. Sharding made sense: their data didn&amp;#39;t fit on one machine. Yours does. The advice isn&amp;#39;t wrong. It&amp;#39;s wrong at your scale.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s the whole ask: measure. Your numbers, not theirs. Don&amp;#39;t engineer at their scale. Engineer at yours.&lt;/p&gt;
</content:encoded></item><item><title>Before you add an Elasticsearch cluster</title><link>https://karloscodes.com/blog/before-you-add-elasticsearch/</link><guid isPermaLink="true">https://karloscodes.com/blog/before-you-add-elasticsearch/</guid><description>Elasticsearch isn&apos;t a feature you add. It&apos;s a system you run. For most of us, Postgres already searches.</description><pubDate>Sun, 31 May 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&amp;#39;ve reached for Elasticsearch more than once without really thinking about it. You need search, ES is what you reach for. It&amp;#39;s the reflex.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s what the reflex skips, and it&amp;#39;s the thing that matters most when you&amp;#39;re small. Elasticsearch isn&amp;#39;t a feature you add. It&amp;#39;s a system you run. Another database sitting next to the one you already have, kept in sync, one more thing to monitor, back up, and get paged for. At 3am, alone, because you are the whole ops team.&lt;/p&gt;
&lt;p&gt;That cost has nothing to do with speed. For a solo founder or a small team, the question was never &amp;quot;is Elasticsearch faster.&amp;quot; It&amp;#39;s &amp;quot;do I want to operate a search cluster to find out.&amp;quot; Usually the honest answer is no.&lt;/p&gt;
&lt;p&gt;Because Postgres already searches, and for the size most of us actually run, it isn&amp;#39;t a compromise.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;ALTER TABLE docs ADD COLUMN search tsvector
  GENERATED ALWAYS AS (
    to_tsvector(&amp;#39;english&amp;#39;, coalesce(title,&amp;#39;&amp;#39;) || &amp;#39; &amp;#39; || coalesce(body,&amp;#39;&amp;#39;))
  ) STORED;

CREATE INDEX docs_search_idx ON docs USING GIN (search);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT id, ts_rank(search, q) AS rank
FROM docs, websearch_to_tsquery(&amp;#39;english&amp;#39;, $1) q
WHERE search @@ q
ORDER BY rank DESC LIMIT 20;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No new service. No sync. The index updates in the same transaction as the write, so stale search results aren&amp;#39;t a bug you can have. It&amp;#39;s a column and an index on the database you already run.&lt;/p&gt;
&lt;p&gt;Does skipping the cluster cost you speed? I measured it, and &lt;a href=&quot;https://github.com/karloscodes/postgres-search-benchmark&quot;&gt;put the whole benchmark online&lt;/a&gt;. A million real product reviews, Postgres and Elasticsearch side by side. This isn&amp;#39;t a lab-grade benchmark and doesn&amp;#39;t try to be. It&amp;#39;s numbers from a Mac Mini M4 running Docker, meant to show roughly how search slows down as the data grows, not to give you exact figures:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;rows&lt;/th&gt;
&lt;th&gt;typical&lt;/th&gt;
&lt;th&gt;worst case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;0.1 ms&lt;/td&gt;
&lt;td&gt;1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;0.8 ms&lt;/td&gt;
&lt;td&gt;12 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;15 ms&lt;/td&gt;
&lt;td&gt;143 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Read the top two rows, because that&amp;#39;s the size most apps actually are. At a hundred thousand documents Postgres answers a typical query in under a millisecond. No human can tell that apart from a dedicated cluster. You would run a second system to save time nobody can notice.&lt;/p&gt;
&lt;p&gt;The bottom row is real, though. At a million rows the worst queries cross 100 ms. But what makes a query slow isn&amp;#39;t the size of the table, it&amp;#39;s how many rows it matches. Your everyday searches stay instant even at a million rows. Only broad common-term queries over a large dataset cost you.&lt;/p&gt;
&lt;p&gt;In practice that ties back to dataset size, and other benchmarks put the working range clearly. Xata recommends starting with Postgres-only search for tables &lt;a href=&quot;https://xata.io/blog/postgres-full-text-search-postgres-vs-elasticsearch&quot;&gt;under 100,000 rows&lt;/a&gt;. Neon, testing at &lt;a href=&quot;https://neon.com/blog/postgres-full-text-search-vs-elasticsearch&quot;&gt;ten million&lt;/a&gt;, found Postgres&amp;#39;s built-in search had gotten too slow by then, while a stronger search index kept up. So the range is roughly this: the built-in search is comfortable into the hundreds of thousands, and once you&amp;#39;re into the millions it&amp;#39;s worth adding something stronger.&lt;/p&gt;
&lt;p&gt;You have two options inside Postgres, and you reach for them in order. First, the built-in full-text search, the code above. Nothing to install, it ships with every Postgres, and for a small-to-medium dataset it&amp;#39;s all you need. The costs are mild: it adds some storage, and its ranking is basic, so it won&amp;#39;t give you finely tuned relevance. Second, &lt;a href=&quot;https://neon.com/blog/postgres-full-text-search-vs-elasticsearch&quot;&gt;&lt;code&gt;pg_search&lt;/code&gt;&lt;/a&gt;, an extension that adds the same kind of ranking Elasticsearch uses, directly to Postgres. In Neon&amp;#39;s test it matched or beat Elasticsearch and handled a single database up to hundreds of millions of rows. The catch is it&amp;#39;s a third-party extension, so you need a Postgres you can install it on.&lt;/p&gt;
&lt;p&gt;So here&amp;#39;s my opinion, plainly. Solo and small-to-mid teams over-engineer search constantly, and it&amp;#39;s almost never about load. It&amp;#39;s fear, or habit, or wanting the resume line. We reach for the cluster before we have the problem the cluster solves.&lt;/p&gt;
&lt;p&gt;Match the tool to your scale. Start with the search that&amp;#39;s built into every Postgres. Into the millions, &lt;code&gt;pg_search&lt;/code&gt; gives you stronger ranking without leaving the database. Elasticsearch comes last, for when you outgrow a single machine: billions of rows, search load you need off your main database, or scale you have to spread across several servers. Those are real reasons, and you&amp;#39;ll know when they&amp;#39;re yours.&lt;/p&gt;
&lt;p&gt;Until then you&amp;#39;re not running Google. You&amp;#39;re running a search box over a dataset that fits in your database. Add one index. Don&amp;#39;t run big-company infrastructure until you have big-company problems.&lt;/p&gt;
</content:encoded></item><item><title>Debug production with AI agents</title><link>https://karloscodes.com/blog/debug-production-with-ai-agents/</link><guid isPermaLink="true">https://karloscodes.com/blog/debug-production-with-ai-agents/</guid><description>The tight loop: error in logs, AI reads it, writes a fix, deploys. Hours become minutes.</description><pubDate>Thu, 05 Mar 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Something breaks. You open the logs. You scroll. You search. You find the error. You context switch to the code. You trace it. You fix it. You deploy. You check if it worked.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s an hour. Maybe two. More if you&amp;#39;re unlucky.&lt;/p&gt;
&lt;p&gt;Now imagine: you tell an AI to check your logs. It finds the error, knows your codebase, writes the fix. You review it or not. Deploy, verify, done.&lt;/p&gt;
&lt;p&gt;Fifteen minutes. Maybe less - the AI can do it all.&lt;/p&gt;
&lt;p&gt;Same outcome. A fraction of the time and effort.&lt;/p&gt;
&lt;h2&gt;The tight loop&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Error in logs → AI reads it → AI writes fix → AI deploys it → AI verifies → Done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The AI has context. It knows your codebase. It sees what broke and why. It can do it all.&lt;/p&gt;
&lt;h2&gt;Why this matters if you&amp;#39;re solo&lt;/h2&gt;
&lt;p&gt;Some bugs are quick. Typo, missing null check, obvious fix.&lt;/p&gt;
&lt;p&gt;Others are nasty. You&amp;#39;re tracing through five files, reading stack traces, trying to reproduce it locally, losing an afternoon.&lt;/p&gt;
&lt;p&gt;The AI doesn&amp;#39;t get tired. It reads the logs, traces the code, and finds the issue. The nasty bugs that steal your afternoon? They become 15-minute problems.&lt;/p&gt;
&lt;h2&gt;What you actually need&lt;/h2&gt;
&lt;p&gt;For this to work, the AI needs access to your errors and logs. Not raw log files scattered across servers. Structured, searchable, linked, grouped. Something with an API the agent can query.&lt;/p&gt;
&lt;p&gt;A tool that groups errors, links related logs, tracks occurrences, shows what happened before and after - that context helps you debug faster. Turns out it helps the AI too. The same structure that makes logs useful for humans makes them useful for agents.&lt;/p&gt;
&lt;p&gt;The rest is straightforward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An LLM with access to your codebase&lt;/li&gt;
&lt;li&gt;A deployment pipeline the agent can trigger&lt;/li&gt;
&lt;li&gt;A way to verify the fix worked&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No fancy APM. No distributed tracing. Just your logs, an AI, and a deploy script.&lt;/p&gt;
&lt;h2&gt;What this enables&lt;/h2&gt;
&lt;p&gt;No scrolling. No context switching. No losing your afternoon.&lt;/p&gt;
&lt;p&gt;This is what AI unlocks for solos with simple stacks. Your small codebase isn&amp;#39;t a limitation - it&amp;#39;s an advantage. The AI can understand it, navigate it, fix it.&lt;/p&gt;
&lt;p&gt;One person. One codebase. One tight loop. &lt;a href=&quot;https://lognorth.com&quot;&gt;LogNorth&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Small Data</title><link>https://karloscodes.com/blog/small-data/</link><guid isPermaLink="true">https://karloscodes.com/blog/small-data/</guid><description>I opened the database. Six months of production web analytics. 14 megabytes.</description><pubDate>Sun, 15 Feb 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I opened Fusionaly&amp;#39;s database yesterday. Six months of production web analytics. Thousands of pageviews, sessions, referrers, all the dashboards.&lt;/p&gt;
&lt;p&gt;14 megabytes.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s absurdly small. I could email the whole thing.&lt;/p&gt;
&lt;p&gt;Turns out that&amp;#39;s all I need.&lt;/p&gt;
&lt;p&gt;I&amp;#39;m building two more products the same way.&lt;/p&gt;
&lt;p&gt;Formlander handles form submissions. Three months in production, same architecture. LogNorth captures errors and logs. Two weeks old, same pattern.&lt;/p&gt;
&lt;p&gt;SQLite for all of them. One file per product. No clusters. No shards.&lt;/p&gt;
&lt;p&gt;The pattern isn&amp;#39;t the file size. The pattern is what I didn&amp;#39;t build.&lt;/p&gt;
&lt;p&gt;No event streaming — SQLite handles my write volume fine. No separate analytics database — I aggregate on write, queries stay fast. No replication — daily VPS snapshots are enough for a business that won&amp;#39;t die if it&amp;#39;s down for an hour.&lt;/p&gt;
&lt;p&gt;The industry would call this unscalable. They&amp;#39;re confusing my scale with Google&amp;#39;s. Google processes billions of events per second. I process thousands per day. That&amp;#39;s their rounding error. To me, it&amp;#39;s a business.&lt;/p&gt;
&lt;p&gt;The math is almost embarrassing.&lt;/p&gt;
&lt;p&gt;A busy site does 50,000 pageviews per day. Ten years of that is 180 million events — somewhere between 50 and 80 gigabytes. With aggregation, retention, and compaction, smaller still.&lt;/p&gt;
&lt;p&gt;Check your production database. How big is it actually? You might be surprised.&lt;/p&gt;
&lt;p&gt;&amp;quot;But what if I outgrow it?&amp;quot;&lt;/p&gt;
&lt;p&gt;You&amp;#39;ll know. The database will tell you. The queries will slow down. The file will get big.&lt;/p&gt;
&lt;p&gt;And when that day comes, you&amp;#39;ll have the revenue to solve it. That&amp;#39;s a good problem. Most businesses never get there.&lt;/p&gt;
&lt;p&gt;What you shouldn&amp;#39;t do is build for imaginary scale today. I wasted years doing that.&lt;/p&gt;
&lt;p&gt;Your data is small.&lt;/p&gt;
&lt;p&gt;So why are you building like it isn&amp;#39;t?&lt;/p&gt;
&lt;p&gt;That&amp;#39;s not a limitation — it&amp;#39;s your leverage. Small enough to self-host. Small enough to own completely.&lt;/p&gt;
&lt;p&gt;Embrace it.&lt;/p&gt;
</content:encoded></item><item><title>SQLite for Web Analytics: How I Made It Work</title><link>https://karloscodes.com/blog/sqlite-for-web-analytics/</link><guid isPermaLink="true">https://karloscodes.com/blog/sqlite-for-web-analytics/</guid><description>Most sites don&apos;t need big data tools. Here&apos;s how SQLite handles web analytics with radical simplicity.</description><pubDate>Fri, 23 Jan 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I like building things to see if they work. Sometimes that means ignoring the standard advice.&lt;/p&gt;
&lt;p&gt;Web analytics usually means ClickHouse, or Kafka, or at least Postgres with TimescaleDB. Big tools for big data. But most sites don&amp;#39;t have big data — they have thousands of visits, maybe tens of thousands. Single-tenant analytics isn&amp;#39;t a scale problem.&lt;/p&gt;
&lt;p&gt;So I wondered: could a plain relational database handle this? And if so, how simple could we make it?&lt;/p&gt;
&lt;h2&gt;Aggregation changes everything&lt;/h2&gt;
&lt;p&gt;Nobody needs millisecond-level granularity in their dashboard. Hourly buckets are fine. And once you aggregate by hour, the numbers get small fast: 24 rows per day per dimension. A year of data fits in megabytes, not gigabytes.&lt;/p&gt;
&lt;p&gt;At 100K visits/day, you&amp;#39;re looking at roughly 26 GB/year. Scale to 500K and it&amp;#39;s 130 GB. Even a million daily visits only hits 260 GB/year. This isn&amp;#39;t big data. This is just... data.&lt;/p&gt;
&lt;p&gt;So yes, any relational database handles this easily. But that got me thinking: if the data is this small, do we even need a database server?&lt;/p&gt;
&lt;h2&gt;What if we used SQLite?&lt;/h2&gt;
&lt;p&gt;SQLite has one limitation that scares people off: concurrent writes. Only one writer at a time. Under heavy load, writes queue up and block each other.&lt;/p&gt;
&lt;p&gt;But analytics doesn&amp;#39;t need instant consistency. Nobody&amp;#39;s refreshing dashboards 50 times per second. What matters is never losing an event. So instead of fighting SQLite&amp;#39;s limitation, I leaned into it — with coordination across layers.&lt;/p&gt;
&lt;h2&gt;The browser buffers&lt;/h2&gt;
&lt;p&gt;The SDK batches events every 200ms and retries three times with exponential backoff. If the network drops, it caches up to 100 events in localStorage. When you close the tab, &lt;code&gt;sendBeacon()&lt;/code&gt; fires off whatever&amp;#39;s left.&lt;/p&gt;
&lt;p&gt;The SDK doesn&amp;#39;t fire and forget. It listens for the server&amp;#39;s response.&lt;/p&gt;
&lt;h2&gt;The server pushes back&lt;/h2&gt;
&lt;p&gt;Sometimes the server says &amp;quot;slow down.&amp;quot; When it&amp;#39;s overwhelmed, it returns &lt;code&gt;503&lt;/code&gt; with a &lt;code&gt;Retry-After&lt;/code&gt; header. The SDK respects that — waits, then tries again. Events don&amp;#39;t drop, they just arrive later.&lt;/p&gt;
&lt;p&gt;This handshake keeps the pace manageable. The browser and server agree on how fast events flow.&lt;/p&gt;
&lt;h2&gt;The database absorbs writes gradually&lt;/h2&gt;
&lt;p&gt;Events land in a staging table — dead simple schema, optimized for fast INSERTs. If SQLite is busy, the write retries up to 10 times with backoff. A background job drains the staging table in batches and rolls everything into hourly aggregates.&lt;/p&gt;
&lt;p&gt;The dashboard reads from those aggregates. Under high load it might lag a few minutes — but you&amp;#39;re not a stock trader. Who cares?&lt;/p&gt;
&lt;p&gt;One process. One file. No Redis queue, no Kafka buffer, no worker cluster. Coordination lives in code, not infrastructure.&lt;/p&gt;
&lt;h2&gt;This isn&amp;#39;t for everyone&lt;/h2&gt;
&lt;p&gt;Could SQLite keep up at a million visits/day? Maybe. But if you&amp;#39;re at that scale, you&amp;#39;re not who I built this for. You have a team. You can afford managed Postgres. Go use Plausible or roll your own.&lt;/p&gt;
&lt;p&gt;This is for the rest of us. Most sites see 10K-50K visits/day — that&amp;#39;s the reality for most projects, most businesses, most side hustles.&lt;/p&gt;
&lt;h2&gt;It scales further than you&amp;#39;d think&lt;/h2&gt;
&lt;p&gt;It&amp;#39;s not 2010 anymore. SQLite on NVMe delivers sub-millisecond writes. The bottleneck everyone worried about a decade ago just isn&amp;#39;t there.&lt;/p&gt;
&lt;p&gt;Start with a 512MB server on Hetzner. When you outgrow it, bump to 1GB. Then 2GB. Then 32GB. Vertical scaling is underrated — no network hops, no distributed coordination, no consistency headaches between nodes.&lt;/p&gt;
&lt;p&gt;Backups? Litestream streams every change to S3 in near-realtime. Point-in-time recovery, disaster recovery — all handled. Or just snapshot the file and ship it somewhere safe.&lt;/p&gt;
&lt;p&gt;Radical simplicity, pragmatic results. One process. One file. If you ever outgrow it, you&amp;#39;ll know exactly when.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Want to try it? &lt;a href=&quot;https://github.com/karloscodes/fusionaly-oss&quot;&gt;Fusionaly is open source on GitHub&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Stop Self-Hosting Interplanetary Rockets</title><link>https://karloscodes.com/blog/stop-self-hosting-interplanetary-rockets/</link><guid isPermaLink="true">https://karloscodes.com/blog/stop-self-hosting-interplanetary-rockets/</guid><description>Why most self-hosted apps are overengineered SaaS clones. Build for one server, not for scale you will never need.</description><pubDate>Sun, 07 Sep 2025 11:52:28 GMT</pubDate><content:encoded>&lt;p&gt;Most self-hosted apps weren’t really built for self-hosting.&lt;br&gt;They’re just the SaaS product, open-sourced and thrown over the wall.&lt;/p&gt;
&lt;p&gt;Which means you inherit everything that made sense for SaaS at scale:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Multi-tenant databases built for millions of users&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Layers of services (Redis, Kafka, Elastic, ClickHouse, background workers, queues…)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ops complexity designed for an SRE team, not one person&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s like buying an interplanetary rocket when all you needed was a car to get across town.&lt;/p&gt;
&lt;p&gt;Sure, it’s impressive. But you don’t need that kind of firepower.&lt;br&gt;You don’t need to launch payloads to Mars.&lt;br&gt;You just want to run your company’s data safely, simply, and on your own terms.&lt;/p&gt;
&lt;h2&gt;The Overkill Problem&lt;/h2&gt;
&lt;p&gt;When you run these “self-hosted” SaaS clones, you’re taking on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Infrastructure designed for global traffic you’ll never see&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Databases meant for multi-tenant complexity you’ll never need&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A maintenance burden you’ll never want&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Self-hosting business tools should give you more control, not more headaches.&lt;br&gt;But too often, it ends up feeling like running AWS from your living room.&lt;/p&gt;
&lt;h2&gt;What Self-Hosting Should Mean&lt;/h2&gt;
&lt;p&gt;Self-hosting should be about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Control&lt;/strong&gt; – you own your data&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Simplicity&lt;/strong&gt; – you can run it on one server&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sustainability&lt;/strong&gt; – you can maintain it yourself&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don’t need an interplanetary rocket.&lt;br&gt;You need a reliable car: easy to drive, easy to keep, gets the job done.&lt;/p&gt;
&lt;h2&gt;A Different Approach&lt;/h2&gt;
&lt;p&gt;That&amp;#39;s why I built &lt;strong&gt;Fusionaly&lt;/strong&gt; this way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;One server&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One payment, own it forever&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bring your own AI key (no hidden usage costs)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Evaluation made simple: one-command devbox, no hoops&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Designed for self-hosting from the start, not retrofitted from SaaS.&lt;/p&gt;
&lt;h2&gt;Closing&lt;/h2&gt;
&lt;p&gt;Self-hosting shouldn’t feel like operating NASA mission control.&lt;br&gt;It should feel like running your own tool, on your own terms.&lt;/p&gt;
&lt;p&gt;Stop self-hosting rockets.&lt;br&gt;Start self-hosting tools you can actually run.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://fusionaly.com&quot;&gt;Learn more about Fusionaly&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Life is a counterbalance act</title><link>https://karloscodes.com/blog/life-is-a-counterbalance-act/</link><guid isPermaLink="true">https://karloscodes.com/blog/life-is-a-counterbalance-act/</guid><description>Balance is a myth. Life is constant counterbalancing - leaning into one thing means leaning away from another.</description><pubDate>Sun, 17 Aug 2025 10:38:08 GMT</pubDate><content:encoded>&lt;p&gt;People say life is about balance. I used to say it too.&lt;/p&gt;
&lt;p&gt;Turns out, it’s not.&lt;/p&gt;
&lt;p&gt;Perfect balance is boring. It’s static. Nothing happens there.&lt;/p&gt;
&lt;p&gt;The good stuff happens at the edges. When you work hard. When you play hard. When you lean all the way in. That’s where growth happens. That’s where the magic is.&lt;/p&gt;
&lt;p&gt;But stay there too long and you burn out. The edge takes everything out of you.&lt;/p&gt;
&lt;p&gt;That’s why life isn’t balance, it’s counterbalance. You push one way, then pull back. You swing between intensity and rest, between yes and no.&lt;/p&gt;
&lt;p&gt;It’s not about sitting in the middle. It’s about moving back and forth with intention. That’s the rhythm of a full life.&lt;/p&gt;
&lt;p&gt;True wisdom is knowing when to push and when to pull back. When to burn bright and when to cool down.&lt;/p&gt;
&lt;p&gt;Life isn’t balance. Life is counterbalance.&lt;/p&gt;
</content:encoded></item><item><title>Don&apos;t forget to sharpen your axe</title><link>https://karloscodes.com/blog/dont-forget-to-sharpen-your-axe/</link><guid isPermaLink="true">https://karloscodes.com/blog/dont-forget-to-sharpen-your-axe/</guid><description>The trap of endless feature building. Sometimes the best thing is to stop and sharpen your tools.</description><pubDate>Sat, 17 May 2025 08:27:45 GMT</pubDate><content:encoded>&lt;p&gt;Every evening, after my day job, I carve out a few hours for my side projects as a part-time solopreneur, and my brain goes wild. A new feature to make my app pop. A slick refactor to clean up that messy code. A whole new tool that could be &lt;em&gt;the one&lt;/em&gt;. It’s like a hundred doors open at once, each whispering, “Pick me.” I love it, the rush of ideas, the itch to tinker. But here’s the truth: it’s a double edged sword.&lt;/p&gt;
&lt;p&gt;For years, I let that flood carry me. I’d code a bit here, sketch a new app there, chase every shiny thought. It felt creative, alive. But nothing shipped. I was running in circles, burning out, and hating myself for it. My side hustle, my escape, became a trap. Too many ideas, not enough focus.&lt;/p&gt;
&lt;p&gt;The hardest lesson I’ve learned is prioritization. Not the sexy kind with color-coded to-do lists, but the brutal kind: picking one thing and ignoring the rest. I started asking, &lt;em&gt;What’s the smallest move with the biggest impact?&lt;/em&gt; One feature that solves a real pain. One bug that’s blocking users. One hour of deep work. It’s not fun, it feels like strangling my curiosity. But it works. I’ve shipped projects I’m proud of, even if they don’t pay my bills yet, because I learned to say no to 99% of my brain’s noise.&lt;/p&gt;
&lt;p&gt;I’m driven, alive, chasing something real. Still, I miss something when I don’t tinker and explore, a spark, a sense of play that keeps my soul lit. So I found a balance. Most nights, I’m ruthless: one task, one goal, no distractions. But I let myself play. Spend time with a new language, sketch a crazy app, or just break something for fun. It’s not productive, and that’s the point. Focus ships; tinkering keeps me human.&lt;/p&gt;
&lt;p&gt;Life’s funny, isn’t it? The thing that makes you effective —prioritization— can dim your spark if you’re not careful. Don’t let it. Make time for something that brings you joy. You’ll ship more and stay alive inside. Just don’t forget to sharpen your axe from time to time.&lt;/p&gt;
</content:encoded></item><item><title>Thriving in the AI era</title><link>https://karloscodes.com/blog/thriving-in-the-ai-era/</link><guid isPermaLink="true">https://karloscodes.com/blog/thriving-in-the-ai-era/</guid><description>How software engineers stay relevant as AI transforms the industry. Focus on judgment, not just code.</description><pubDate>Sat, 08 Feb 2025 09:30:26 GMT</pubDate><content:encoded>&lt;p&gt;My Vision as a Software Engineering in the AI Era&lt;/p&gt;
&lt;p&gt;Core Beliefs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;As a techno-optimist, I believe that AI amplifies human potential rather than replacing it&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The future belongs to those who learn to leverage AI while maintaining their agency&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Engineering excellence will be redefined by judgment, not just coding skills&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Our most valuable assets remain uniquely human: critical thinking, principles, and values&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Be you, nobody can beat you at that, authenticity becomes an even stronger differentiator in an AI world&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Evolution of Skills:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Traditional role boundaries are dissolving, being &amp;quot;just&amp;quot; a backend engineer is limiting&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AI tools give us &amp;quot;superpowers&amp;quot;, the ability to confidently work across the entire stack&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deep understanding still matters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Fundamentals are non-negotiable&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Learning comes from books, reflection, and writing&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AI is a tool, not a replacement for knowledge&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Focus shifts to higher-level capabilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Strategic thinking&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;System design&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Decision-making&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Technical judgment&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Clear communication&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Industry Shifts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Teams get smaller but more impactful:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Fewer engineers, but more versatile&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Full-stack renaissance&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Blending of engineering, product, and design roles&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Market dynamics intensify:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;More companies, smaller sizes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Increased competition&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Network becomes crucial for opportunities&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keys to Success:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Master AI collaboration without becoming dependent&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Build deep foundations in computer science and engineering principles&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Develop strong problem-solving, decision-making and strategic thinking skills&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Excel at communication:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Articulate complex ideas simply&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Write clear, compelling documentation&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bridge technical and non-technical perspectives&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Build influence through effective communication&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cultivate your authentic voice and perspective, authenticity is your competitive advantage&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Build a robust professional network and brand&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Never stop learning, but learn strategically&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Writing this has been an exercise in optimistic futurism. As someone who believes in the positive potential of technology and the opportunities AI will create, I&amp;#39;m genuinely excited to see how these thoughts age. Future me, if you&amp;#39;re reading this in 2030, I hope you&amp;#39;re smiling at how far we&amp;#39;ve come. Time will tell!&lt;/p&gt;
</content:encoded></item><item><title>Total Cost of Ownership, a software engineering perspective</title><link>https://karloscodes.com/blog/total-cost-of-ownership-software-engineering/</link><guid isPermaLink="true">https://karloscodes.com/blog/total-cost-of-ownership-software-engineering/</guid><description>Total Cost of Ownership, a software engineering perspective</description><pubDate>Sat, 14 Dec 2024 13:14:08 GMT</pubDate><content:encoded>&lt;p&gt;Engineering is fundamentally about trade-offs. Every decision carries weight, implications, and most importantly, costs. When talking about costs in software engineering, we&amp;#39;re not just dealing with financial expenses, we&amp;#39;re considering time investment, cognitive overhead, operational complexity, and team resources. While software engineers excel at discussing technical merits, architectural patterns, and code quality, there&amp;#39;s often a blind spot when it comes to understanding and considering the &lt;a href=&quot;https://en.wikipedia.org/wiki/Total_cost_of_ownership&quot;&gt;Total Cost of Ownership&lt;/a&gt; (TCO) of our solutions.&lt;/p&gt;
&lt;h2&gt;The developer cost blind spot&lt;/h2&gt;
&lt;p&gt;Software engineers are trained to value technical excellence: clean code, elegant architectures, and engineering purity. This mindset, while valuable, often creates a blind spot when evaluating solutions. We excel at discussing algorithmic complexity, scalability, and code maintainability, but frequently overlook or underestimate the total cost of ownership of our technical decisions.&lt;/p&gt;
&lt;p&gt;This blind spot manifests in how we evaluate solutions. We might build a technically superior approach without considering its maintenance burden, or choose cutting-edge technology without factoring in the cost of keeping the team proficient with it. Cost optimization isn&amp;#39;t about choosing the cheapest solution, it&amp;#39;s about understanding the full spectrum of costs: development effort, maintenance burden, operational complexity, team expertise, the price of future changes and how they all compound in time.&lt;/p&gt;
&lt;h2&gt;The hidden costs in our solutions&lt;/h2&gt;
&lt;p&gt;What makes a solution good from an engineering perspective doesn&amp;#39;t always align with what makes it sustainable and efficient in the long run. A beautifully designed microservice architecture might be technically elegant, but the operational complexity, infrastructure demands, and cognitive overhead of maintaining multiple services could turn it into an overwhelming burden for the team.&lt;/p&gt;
&lt;p&gt;To showcase this concept more clearly, let&amp;#39;s examine a simpler example that many developers face: building a personal blog, comparing building everything yourself versus hosting your posts on a blogging platform.&lt;/p&gt;
&lt;p&gt;From a pure TCO perspective, both approaches require an initial investment, though they differ significantly in scope and time to market:&lt;/p&gt;
&lt;h3&gt;Initial Investment&lt;/h3&gt;
&lt;p&gt;Custom blog:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Research time: Comparing static site generators, hosting options, and deployment strategies&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Learning curve: Understanding the chosen technology stack&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Implementation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Basic: Theme selection/creation, content structure&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Advanced: SEO optimization, tagging system&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Infrastructure: Domain setup, deployment pipeline&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Features: RSS feeds, contact forms, email subscriptions&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Initial deployment and testing&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Managed platform:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Research time: Comparing available platforms&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Learning curve: Platform-specific features&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Basic: Theme selection, content structure&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Advanced: SEO settings, tagging system&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Infrastructure: Domain connection&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Features: Built-in RSS, forms, subscriptions&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The cost different is already quite evident, especially in terms of time investment. The time to market is significantly better with the managed platform, it can have you publishing content within hours, while a custom solution might take weeks to reach the same functionality level.&lt;/p&gt;
&lt;h3&gt;Ongoing costs over time&lt;/h3&gt;
&lt;p&gt;The true cost difference becomes evident as time passes. A custom blog requires constant attention: security updates, dependency management, framework upgrades, and fixing broken features. Each year brings new challenges: major version migrations, growing technical debt, and possibly even complete rebuilds as technologies become obsolete.&lt;/p&gt;
&lt;p&gt;Meanwhile, a managed platform quietly evolves, with improvements and new features appearing automatically. The time saved compounds, hours not spent on maintenance become hours invested in creating value.&lt;/p&gt;
&lt;h3&gt;The opportunity cost&lt;/h3&gt;
&lt;p&gt;The most significant hidden cost manifests in the opportunities foregone while managing infrastructure (&lt;a href=&quot;https://en.wikipedia.org/wiki/Opportunity_cost&quot;&gt;Opportunity cost&lt;/a&gt;). Those hours spent troubleshooting deployment issues could have produced new articles. The mental energy devoted to maintaining dependencies could have gone into engaging with readers or growing your professional network. Time invested in upgrading frameworks could have been spent developing expertise in your core domain or building valuable industry relationships. These missed opportunities accumulate over time, often far exceeding the visible costs of either solution.&lt;/p&gt;
&lt;p&gt;The TCO difference becomes substantial when accounting for these opportunity costs. While building a custom blog might serve additional purposes like learning specific technologies or having complete control over the platform, from a pure cost perspective, the managed solution often wins, not just in direct costs, but in freeing up time and mental energy for higher-value activities.&lt;/p&gt;
&lt;h2&gt;Building a cost-conscious engineering culture&lt;/h2&gt;
&lt;p&gt;Cost consciousness must become an integral part of the software engineering mindset, as poor cost assessment can lead to significant problems down the line. Solutions may become unsustainable as they scale, technical debt accumulates faster than expected, and valuable resources get drained from maintaining over-engineered systems. Most critically, high operational costs can limit business opportunities and make it difficult to pivot when needed.&lt;/p&gt;
&lt;p&gt;When engineers factor in cost considerations from the start, they naturally gravitate toward more pragmatic solutions. This doesn&amp;#39;t mean always choosing the cheapest option, rather, it&amp;#39;s about understanding the long-term implications of technical decisions. A more expensive initial investment might lead to lower maintenance costs, while a seemingly cheap solution could become a resource drain over time.&lt;/p&gt;
&lt;p&gt;Consider introducing a &amp;quot;TCO check&amp;quot; in your architectural decision records or design reviews. For each significant technical decision, include a section that answers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;What&amp;#39;s the initial development cost?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What ongoing maintenance will be required?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What risks might we face in six months or a year?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What alternatives did we consider and what were their cost implications?&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Make TCO reviews a regular part of your engineering practices and establish a clear ownership of it. Teams should be accountable for their cost decisions, just as they are for their service reliability. This simple practice helps teams develop a habit of considering the complete cost picture and make it part of their engineering workflow.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Cost isn&amp;#39;t just a business concern, it&amp;#39;s a fundamental engineering constraint that deserves the same attention as performance, scalability, and maintainability. The most elegant solution might not be the right one if its TCO makes it impractical or unsustainable. This might mean choosing a managed service over building a custom solution, using established technologies over cutting-edge ones, or accepting some technical compromises to meet business constraints. Of course, every engineering decision depends on context, sometimes building your own solution is the right strategic choice, but that&amp;#39;s a story for another post.&lt;/p&gt;
&lt;p&gt;By embracing cost consciousness in software engineering decisions, we build not just better systems, but more viable and sustainable ones. The next time you&amp;#39;re evaluating a technical decision, remember to factor in the total cost of ownership, your future self and your company, will thank you.&lt;/p&gt;
</content:encoded></item><item><title>New and trendy: Convenience at what cost?</title><link>https://karloscodes.com/blog/new-and-trendy-convenience-at-what-cost/</link><guid isPermaLink="true">https://karloscodes.com/blog/new-and-trendy-convenience-at-what-cost/</guid><description>The hidden costs of chasing new frameworks and tools. Why boring technology often wins.</description><pubDate>Sat, 07 Dec 2024 12:18:59 GMT</pubDate><content:encoded>&lt;p&gt;The software industry is obsessed with the new and trendy. Every few months, a new tool, framework, or methodology emerges, promising to revolutionize how we build software. While innovation is essential and drives progress, the adoption of new and rapid changing technologies creates substantial hidden costs that we rarely discuss. The key isn&amp;#39;t to avoid innovation, but to understand how these choices impact our organizations over time, impacts that often only become apparent years later, when the accumulated weight of our decisions begins to show.&lt;/p&gt;
&lt;h2&gt;Compounding effect of change rates&lt;/h2&gt;
&lt;p&gt;What&amp;#39;s often overlooked in technology decisions is how the rate of change itself becomes a critical risk factor. Technologies that change rapidly create a compounding burden on organizations. This isn&amp;#39;t just about keeping up with the latest versions, it&amp;#39;s about the multiplicative effect of change across an entire technology stack.&lt;/p&gt;
&lt;p&gt;This isn&amp;#39;t an argument against innovation or new technologies, many truly revolutionary tools have transformed our industry for the better. Rather, it&amp;#39;s about understanding the full cost of adopting technologies that change rapidly, especially in critical parts of our stack. The faster a technology evolves, the more resources we need to dedicate to keeping up with its ecosystem.&lt;/p&gt;
&lt;p&gt;Consider a modern web application. If each layer of the stack, frontend framework, state management, API layer, database, deployment platform, changes significantly every two years, teams aren&amp;#39;t dealing with linear change. They&amp;#39;re facing an exponential increase in complexity and maintenance burden. A change in one layer, often forces changes in adjacent layers, creating a cascade of updates that can paralyze development teams.&lt;/p&gt;
&lt;p&gt;Not to mention that a major version upgrade of any core technology typically implies hundreds of thousands of dollars in immediate refactoring costs or an equivalent amount in technical debt waiting to be paid.&lt;/p&gt;
&lt;h2&gt;The real cost of rapid changing technologies&lt;/h2&gt;
&lt;p&gt;The real cost lies in how these changes compound over time, in the opportunity cost of delayed features, in the impediments to move forward or in the cost of not adapting to changes.&lt;/p&gt;
&lt;p&gt;The total costs manifest in multiple ways:&lt;/p&gt;
&lt;h3&gt;Direct costs&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Time spent implementing migrations&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Time spent creating release plans&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Time spent in training and skill development&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Time spent in testing and quality assurance&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Hidden Costs&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Lost productivity during learning curves&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Opportunity cost of delayed features&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Risk of introducing new bugs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The risk of increased operational complexity&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The risks of not adapting to changes&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The cost of inaction&lt;/h2&gt;
&lt;p&gt;While chasing every new trend carries significant costs, once you&amp;#39;ve committed to a technology, refusing to adapt to its evolution can be even more expensive. Each skipped update makes the next migration more difficult. What might have been a week-long update can become a year-long rewrite if delayed too long, talent pool starts shrinking, developers starts demotivating. Quite a challenge.&lt;/p&gt;
&lt;h2&gt;The abandonment risk&lt;/h2&gt;
&lt;p&gt;There&amp;#39;s another hidden cost that haunts trendy technology choices: the risk of complete abandonment. When a new technology fails to gain long-term traction, companies who adopted it early face difficult decisions. Popular JavaScript frameworks have disappeared overnight, taking their ecosystems with them. Suddenly, you&amp;#39;re not just maintaining your application, you&amp;#39;re maintaining your foundations.&lt;/p&gt;
&lt;h2&gt;The power of Lindy technology&lt;/h2&gt;
&lt;p&gt;But there&amp;#39;s hope. The &lt;a href=&quot;https://modelthinkers.com/mental-model/the-lindy-effect&quot;&gt;Lindy Effect&lt;/a&gt; suggests that the future life expectancy of non-perishable things like technology is proportional to their current age. In other words, technologies that have been around for a long time are likely to stay around for longer. This isn&amp;#39;t just theoretical, it&amp;#39;s a powerful framework for evaluating technology choices.&lt;/p&gt;
&lt;p&gt;Consider these examples:&lt;/p&gt;
&lt;p&gt;SQL databases have outlived countless NoSQL alternatives that promised to replace them. While NoSQL has its place, organizations that chose PostgreSQL over the latest document store trend found themselves with a battle-tested solution that scales predictably and maintains data integrity, critical factors that become apparent only years into a project.&lt;/p&gt;
&lt;p&gt;Traditional server-side rendering and HTML continue to power some of the web&amp;#39;s most reliable systems, while many Single Page Application frameworks have come and gone. Companies that built complex SPAs often find themselves rewriting their applications every few years, while server-rendered HTML continues to work reliably across all platforms with minimal maintenance.&lt;/p&gt;
&lt;p&gt;These technologies haven&amp;#39;t merely survived; they&amp;#39;ve thrived. They&amp;#39;ve been battle-tested across countless scenarios, their failure modes are well understood, and their documentation is comprehensive. Most importantly, they remain stable and often come with Long Term Support cycles, providing predictable upgrade paths and security updates.&lt;/p&gt;
&lt;h2&gt;Strategic innovation vs trend-chasing&lt;/h2&gt;
&lt;p&gt;The key to successful technology adoption lies in distinguishing between strategic innovation and trend-chasing. Before adopting new technology, organizations should ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;What&amp;#39;s the ecosystem&amp;#39;s stability and long-term outlook?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What problem does it truly solve? Is there a Lindy alternative?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What role will it play? it worth the risks?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What is its rate of change, can we keep up with it?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Do we have the resources to maintain it as it evolves?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How will this decision age over the years?&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In an industry that often equates &amp;quot;new and trendy&amp;quot; with &amp;quot;better,&amp;quot; choosing stable technology can feel counterintuitive. However, success is about making thoughtful, pragmatic technology choices that acknowledge the true cost of change. At the end, you are not Google, and you don&amp;#39;t have the budget to innovate like it.&lt;/p&gt;
&lt;p&gt;Innovation is crucial for progress, but the most successful organizations are those that can distinguish between transformative changes worth the investment and trends that might not justify their long-term costs.&lt;/p&gt;
&lt;p&gt;The key is finding the right balance between innovation and stability. Remember: your users don&amp;#39;t care about how cutting-edge your technology stack is. They care about reliable, efficient solutions to their problems. In many cases, proven technology is the best way to deliver exactly that.&lt;/p&gt;
</content:encoded></item><item><title>Dependencies: Convenience at what cost?</title><link>https://karloscodes.com/blog/dependencies-convenience-at-what-cost/</link><guid isPermaLink="true">https://karloscodes.com/blog/dependencies-convenience-at-what-cost/</guid><description>Every dependency is a liability. When to pull in packages vs. write it yourself.</description><pubDate>Mon, 04 Nov 2024 20:57:29 GMT</pubDate><content:encoded>&lt;p&gt;Installing dependencies in our projects is easy and convenient. With just a few installation steps, you can solve complex problems that would take time to implement from scratch. But is it always a good investment?&lt;/p&gt;
&lt;p&gt;Dependencies hide complexity from us, but that doesn&amp;#39;t mean we should ignore what comes in the package. On the contrary, understanding the full scope of each dependency becomes crucial, the devil is in the details.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1730751209607/103b285b-5f64-4603-bf04-6f3167a46d84.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;And, it&amp;#39;s not just about the direct dependency you&amp;#39;re installing. What about its dependencies? And the dependencies of those dependencies? Who maintains each one of them? &lt;strong&gt;There&amp;#39;s a dangerous multiplier effect here, hidden in plain sight and often ignored&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;The real problems&lt;/h2&gt;
&lt;p&gt;This web of direct/indirect dependencies creates several critical issues:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security vulnerabilities&lt;/strong&gt;: A critical security issue buried six levels deep in a dependency chain can compromise an entire application. The more dependencies a project has, the larger its attack surface becomes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Maintenance overhead&lt;/strong&gt;: Package updates are never isolated events. Dependencies conflict with each other regularly. Package A might require version 1.x of Package C, while Package B demands version 2.x, creating complex dependency resolution challenges that waste development and maintenance time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build bloat&lt;/strong&gt;: You are carrying the baggage of each dependency and its dependencies, making the dependency tree larger than expected. Simple projects can end up with hundreds of megabytes of dependencies. Anyone familiar with those projects where node_modules is larger than the actual application code by a factor of 100?&lt;/p&gt;
&lt;h2&gt;What to do?&lt;/h2&gt;
&lt;p&gt;Smart dependency management requires a strategic approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Question every new dependency. Does the functionality justify the overhead of another package? Could this be implemented in-house with reasonable effort?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use dependency analysis tools, review the source code and repository health of each new dependency you add to your project, checking:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;How actively is it maintained?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How many open issues and pull requests?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What&amp;#39;s the test coverage?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How many contributors?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How many dependencies?&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Maintain regular and automated update schedules while carefully evaluating major version updates.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Implement version locking through common tools like package-lock.json or yarn.lock, etc.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Monitor dependency health continuously:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Set up automated security scanning&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Watch for deprecated packages&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Document dependencies that require special attention&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Sustainability is hard but worth it&lt;/h2&gt;
&lt;p&gt;Optimizing for the short term is not enough. While it&amp;#39;s easy to bring in a dependency to fix an immediate problem, it might not be the best solution for your project&amp;#39;s long-term health. This isn&amp;#39;t an argument for zero-dependency projects, you shouldn&amp;#39;t always reinvent the wheel. The key is thoroughly evaluating the benefits and risks of new dependencies.&lt;/p&gt;
&lt;p&gt;Dependency management is about exercising balance –a lost art in software engineering. It&amp;#39;s about making informed decisions that consider both immediate needs and long-term sustainability. After all, today&amp;#39;s quick fix shouldn&amp;#39;t become tomorrow&amp;#39;s maintenance nightmare.&lt;/p&gt;
</content:encoded></item><item><title>Two phased deployments are useful</title><link>https://karloscodes.com/blog/two-phased-deployments-are-useful/</link><guid isPermaLink="true">https://karloscodes.com/blog/two-phased-deployments-are-useful/</guid><description>Breaking deployments into deploy and activate phases. A simple pattern for safer releases.</description><pubDate>Wed, 25 Sep 2024 16:35:10 GMT</pubDate><content:encoded>&lt;p&gt;Nowadays, zero-downtime deployments are the norm in the software industry. Strategies like &lt;a href=&quot;https://docs.aws.amazon.com/whitepapers/latest/overview-deployment-options/rolling-deployments.html&quot;&gt;Rolling updates&lt;/a&gt;, &lt;a href=&quot;https://martinfowler.com/bliki/CanaryRelease.html?ref=wellarchitected&quot;&gt;Canary releases&lt;/a&gt;, and &lt;a href=&quot;https://docs.aws.amazon.com/whitepapers/latest/overview-deployment-options/bluegreen-deployments.html&quot;&gt;Blue-green deployments&lt;/a&gt; have become ubiquitous, being used by literally everyone and every platform out there. These methods allow updating applications with little to no perceived downtime, seamlessly transitioning users to new versions.&lt;/p&gt;
&lt;p&gt;However, while these strategies effectively address the downtime part of the equation, they shift the trade-offs elsewhere. All of these deployment approaches require having multiple copies of the application, gradually rolling out changes to a subset of servers or pods while the old version continues to run on others. This process ensures service availability throughout the deployment, but it also means that two versions of the application must coexist temporarily within the same infrastructure, which has several implications.&lt;/p&gt;
&lt;h2&gt;Common issues&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Case study: Deleting a table column with zero downtime using rolling strategy.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This operation is typically done in a single step: the code and migrations work together to delete the column from the database and stop using it in the code.&lt;/p&gt;
&lt;p&gt;During the deployment window, which could last from a few seconds to minutes in a rolling deployment, a critical issue emerges. Once the new image is deployed and migrations are applied, the new version of code could be executed on some replicas while the old version runs on others. The old version begins to throw errors if it tries to access the already deleted column, while the new version works correctly.&lt;/p&gt;
&lt;p&gt;This dichotomy generates error spikes during deployments and, in general, could lead to data corruption, inconsistencies, and outages. Most alarmingly, these are not the only problems. Performing such an invasive change in one step implies a bigger risk: it eliminates the possibility of a clean rollback, only allowing fast-forward error solving.&lt;/p&gt;
&lt;p&gt;Fortunately, there’s a better way:&lt;/p&gt;
&lt;h2&gt;The Two-Phased Solution&lt;/h2&gt;
&lt;p&gt;To address these shortcomings and mitigate the associated risks, a two-phased deployment approach becomes crucial. This is not just a fancy name; it literally means two separate deployments, usually separated by a time window. It works like this:&lt;/p&gt;
&lt;h3&gt;Phase 1: Code adaptation&lt;/h3&gt;
&lt;p&gt;The first phase involves deploying new code that doesn&amp;#39;t use the soon-to-be-deleted database column. However, the column still exists in the database at this stage. This allows both old and new code to run without errors, ensuring a smooth transition or rollback if needed.&lt;/p&gt;
&lt;h3&gt;Phase 2: Schema change&lt;/h3&gt;
&lt;p&gt;Once it’s safe, it’s time for the second phase. This involves deploying the change to delete the unused column.&lt;/p&gt;
&lt;h2&gt;Benefits of Two-Phased deployments&lt;/h2&gt;
&lt;p&gt;Implementing two-phased deployments ensures more reliable and reversible updates to applications. This approach offers several benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It provides a smooth transition between versions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It allows the ability to do safe rollbacks&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It significantly reduces the risk of data inconsistencies or application errors.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Beyond a table column deletion&lt;/h2&gt;
&lt;p&gt;While we&amp;#39;ve focused on column deletion, two-phased deployments can be generalized to implement any sort of changes, making the separation into phases a powerful concept for reliable engineering practices.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Two-phased deployments fill a critical gap between zero downtime and reliability, allowing teams to navigate complex updates in a simpler way. By separating the process into phases, they minimize risks and improve overall system stability. Ultimately, this approach ensures that applications can evolve gracefully, maintaining both performance and data integrity throughout the deployment process.&lt;/p&gt;
</content:encoded></item><item><title>Database transactions edge cases</title><link>https://karloscodes.com/blog/landmines-hidden-in-transactions/</link><guid isPermaLink="true">https://karloscodes.com/blog/landmines-hidden-in-transactions/</guid><description>Database transactions edge cases</description><pubDate>Sun, 05 Nov 2023 12:08:55 GMT</pubDate><content:encoded>&lt;p&gt;Transactions look simple. They&amp;#39;re not. Especially when you mix database operations with external I/O like emails, APIs, or payment gateways. This is where things break.&lt;/p&gt;
&lt;p&gt;Let me show you the progression from naive to robust — and why you might not need the complex solution.&lt;/p&gt;
&lt;h2&gt;The naive approach: I/O inside transactions&lt;/h2&gt;
&lt;p&gt;Consider a bank transfer: debit one account, credit another, send confirmation email.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;try {
  begin_transaction()
  debit_source_account()
  credit_destination_account()
  send_email_notification()    // 💣
  commit_transaction()
} catch (error) {
  rollback_transaction()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looks clean. But that email call is a landmine.&lt;/p&gt;
&lt;h3&gt;What goes wrong&lt;/h3&gt;
&lt;p&gt;By default, most databases use &lt;strong&gt;pessimistic locking&lt;/strong&gt; — when you read or write a row inside a transaction, that row is locked until commit. Other transactions wait.&lt;/p&gt;
&lt;p&gt;Now add a slow email server:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Row blocking&lt;/strong&gt;: Other transactions trying to access those accounts wait for the lock to release.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deadlocks&lt;/strong&gt;: Transaction A waits for a row locked by Transaction B, while B waits for a row locked by A. Neither proceeds.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Connection exhaustion&lt;/strong&gt;: Each transaction holds a database connection. Slow I/O means connections pile up. Pool hits zero. Everything stops.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1699185162200/5f2bf5b7-c703-4b06-a84b-2c2c22205268.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;One slow email server takes down your whole application. The domino effect is real.&lt;/p&gt;
&lt;h3&gt;The rollback problem&lt;/h3&gt;
&lt;p&gt;There&amp;#39;s another issue. Consider an e-commerce checkout:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;try {
  begin_transaction()
  update_inventory()
  charge_credit_card()         // 💣
  update_order_status()
  commit_transaction()
} catch (error) {
  rollback_transaction()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What if &lt;code&gt;update_order_status&lt;/code&gt; fails? The database rolls back — but the credit card was already charged. You can&amp;#39;t rollback an HTTP call. Now you have money taken and no order record. Manual cleanup required.&lt;/p&gt;
&lt;h2&gt;Better: Optimistic locking&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Pessimistic locking&lt;/strong&gt;: Lock rows during the transaction. Others wait.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Optimistic locking&lt;/strong&gt;: Don&amp;#39;t lock anything. Check for conflicts at commit time using version numbers.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s how it works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read records and note their version numbers&lt;/li&gt;
&lt;li&gt;Do your work (including slow I/O)&lt;/li&gt;
&lt;li&gt;At update time, check if versions changed&lt;/li&gt;
&lt;li&gt;If unchanged, commit. If changed, retry.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;try {
  begin_transaction()

  // Read with versions
  source = get_account(source_id)        // version: 5
  dest = get_account(dest_id)            // version: 3

  // Slow I/O — but no rows are locked
  send_email_notification()

  // Update with version check
  debit(source, amount, expected_version: 5)
  credit(dest, amount, expected_version: 3)

  commit_transaction()
} catch (version_conflict) {
  rollback_transaction()
  retry()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What this solves&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No row blocking&lt;/strong&gt;: Other transactions read and write freely&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No deadlocks&lt;/strong&gt;: Nothing to deadlock on&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better concurrency&lt;/strong&gt;: Multiple transactions proceed in parallel&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;What this doesn&amp;#39;t solve&lt;/h3&gt;
&lt;p&gt;You&amp;#39;re still holding a database connection for the duration of the transaction. If the email server is slow and you have many concurrent requests, you can still exhaust your connection pool.&lt;/p&gt;
&lt;p&gt;Optimistic locking works well for most applications. But if you need guaranteed delivery and maximum throughput, there&amp;#39;s one more level.&lt;/p&gt;
&lt;h2&gt;Full solution: Transactional outbox&lt;/h2&gt;
&lt;p&gt;The outbox pattern separates &amp;quot;what needs to happen&amp;quot; from &amp;quot;making it happen.&amp;quot;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;try {
  begin_transaction()
  update_inventory()
  add_to_outbox(&amp;quot;charge_credit_card&amp;quot;, order_id)
  add_to_outbox(&amp;quot;send_confirmation&amp;quot;, order_id)
  commit_transaction()
} catch (error) {
  rollback_transaction()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The transaction is fast — just database writes. A separate process handles the I/O:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;// Background worker
process_outbox() {
  for each pending_operation {
    execute(operation)
    mark_completed(operation)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What this solves&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fast transactions&lt;/strong&gt;: No I/O, no waiting&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No connection exhaustion&lt;/strong&gt;: Transactions complete quickly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Guaranteed delivery&lt;/strong&gt;: Outbox persists until processed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retry built-in&lt;/strong&gt;: Failed operations stay in queue&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dead letter queue&lt;/strong&gt;: Errors go somewhere visible&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Trade-off&lt;/h3&gt;
&lt;p&gt;Eventual consistency. The email doesn&amp;#39;t send immediately — it sends when the worker gets to it. For most use cases, a few seconds delay is fine. For some, it&amp;#39;s not.&lt;/p&gt;
&lt;h2&gt;Choose wisely&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Row blocking&lt;/th&gt;
&lt;th&gt;Deadlocks&lt;/th&gt;
&lt;th&gt;Connection exhaustion&lt;/th&gt;
&lt;th&gt;Rollback safety&lt;/th&gt;
&lt;th&gt;Complexity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Naive (pessimistic + I/O)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optimistic locking&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transactional outbox&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Most apps&lt;/strong&gt;: Just move I/O after the commit. Done.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;High concurrency&lt;/strong&gt;: Optimistic locking.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mission-critical delivery&lt;/strong&gt;: Outbox pattern.&lt;/p&gt;
&lt;p&gt;Don&amp;#39;t reach for the complex solution until you have the complex problem.&lt;/p&gt;
</content:encoded></item><item><title>Readability through Conceptual Compression</title><link>https://karloscodes.com/blog/readability-through-conceptual-compression/</link><guid isPermaLink="true">https://karloscodes.com/blog/readability-through-conceptual-compression/</guid><description>Readability through Conceptual Compression</description><pubDate>Sat, 11 Jul 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Code is more read than written. I see programming as a two-fold task: giving machines instructions and making those instructions clear to people. You can tell a difference when someone wrote code to help you read it, they use semantic variable names, define the right boundaries, it&amp;#39;s clean, basically a joy. But, what makes it feel this way? Let me explore in this post what I think makes code a pleasure to read:&lt;/p&gt;
&lt;h4&gt;What takes to easily understand a piece of code&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Strong foundational knowledge in computer science, etc&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Proficiency in the programming language&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Knowledge about the business domain&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Understanding of the problem&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Capturing the intent of how the code solves the problem&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For programmers, the key lies in the last three: how well your code bridges the business domain and the problem. The real challenge is to explain a solution via abstractions and their interactions, in other words, expressing intent. Your code needs to speak, and as a real book, it should be a consistent, balanced, and focused story.&lt;/p&gt;
&lt;h3&gt;Conceptual compression: Intro&lt;/h3&gt;
&lt;p&gt;One resource I use to express intent is Conceptual Compression. Conceptual compression is about reducing complexity through abstraction. It allows multiple elements of a concept to be encapsulated in a more manageable form so they can be reused without explaining it again. Like the term &amp;quot;Quidditch&amp;quot; in the Harry Potter, reusing the book analogy.&lt;/p&gt;
&lt;p&gt;My argument is that compressing portions of code into concepts, and making then interact in the right abstraction level, helps to write code that it&amp;#39;s easy to understand.&lt;/p&gt;
&lt;p&gt;How can it be achieved with different language constructs?&lt;/p&gt;
&lt;h3&gt;Variable naming: A quick win&lt;/h3&gt;
&lt;p&gt;With variables, it&amp;#39;s easy to create abstractions in small fractions of code.&lt;/p&gt;
&lt;p&gt;The following piece of code uses some implementation details to compose an important business logic through a (complex) conditional:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const user =  /* ... */;
if (project.ownerId === user.userId || user.role === &amp;#39;MANAGE_PROJECTS&amp;#39;) { /* ... */ }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here&amp;#39;s an improvement using better abstractions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const currentUser = /* ... */;
const canManageProjects = currentUser.role === &amp;#39;MANAGE_PROJECTS&amp;#39;;
const isProjectOwner = project.ownerId === currentUser.userId;

if (isProjectOwner || canManageProjects) { /* ... */ }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the second version, the compressed concepts optimize the clarity of the conditional, making it look almost in plain English. This is powerful to make things easier to understand.&lt;/p&gt;
&lt;h3&gt;Functions for clarity&lt;/h3&gt;
&lt;p&gt;When abstractions need to travel across function boundaries, functions can also work for encapsulating them. Continuing with the previous example, what if now the code is part of an endpoint?&lt;/p&gt;
&lt;p&gt;The following example makes use of variable concept compression, but it&amp;#39;s not enough, there&amp;#39;s a mix of abstraction levels making the whole function difficult to understand:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;app.put(&amp;#39;/project/:projectId&amp;#39;, async ({ request, response }) =&amp;gt; {
    const user = /* ... */;
    const project = /* ... */;
    const canManageProjects = user.role === &amp;#39;MANAGE_PROJECTS&amp;#39;;
    const isProjectOwner = project.ownerId === user.userId;

    if (isProjectOwner || canManageProjects) { /* ... */ }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My improved version:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function getCurrentUser(request) {
    /* ... */
}

function canManageProjects(user) {
    return user.role === &amp;#39;MANAGE_PROJECTS&amp;#39;;
}

function isProjectOwner(user, project) {
    return project.ownerId === user.userId;
}

app.put(&amp;#39;/project/:projectId&amp;#39;, async ({ request, response }) =&amp;gt; {
    const user = getCurrentUser(request);
    const project = /* ... */;

    if (isProjectOwner(user, project) || canManageProjects(user)) { 
        /* ... */ 
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&amp;#39;s much better now! The endpoint implementation it&amp;#39;s simpler, and it clearly expresses its purpose with unneeded details.&lt;/p&gt;
&lt;h3&gt;Modules for scalability&lt;/h3&gt;
&lt;p&gt;Encapsulating concepts in different files starts becoming a more complex problem. There are different levels in the strength of the connection between different files and their concepts (topic for a different post). Regardless of that, the need to compress similar concepts remains to define what goes together and what not.&lt;/p&gt;
&lt;p&gt;Different languages have different constructs to abstract or encapsulate functionality in files, like packages, classes, or modules. I&amp;#39;ll call them Modules for simplicity.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s a continuation of the hypothetical use case I analyzed before using modules.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// File: src/projects/policies/canUpdateProjectPolicy
function canManageProjects(actor) {
    return actor.role === &amp;#39;MANAGE_PROJECTS&amp;#39;;
}

function isProjectOwner(actor, project) {
    return project.ownerId === actor.userId;
}

export function canUpdateProjectPolicy({ actor, project }) {
    return isProjectOwner(actor, project) || canManageProjects(actor)) { 
}

//----------------------------------------------------------------
    
// File: src/projects/use-cases/update-project.use-case.js
import canUpdateProjectPolicy from &amp;#39;src/projects/policies/can-update-project.policy&amp;#39;;
import ProjectManager from &amp;#39;src/projects/...&amp;lt;not-relevant-now&amp;gt;&amp;#39;;

export async function updateProjectUseCase({ projectId, payload, actor }) {
  const project = ProjectManager.find(projectId);
  if (canUpdateProjectPolicy({ actor, project }) {
      return ProjectManager.update(id, params);
  }
  /* ... */;
};

//----------------------------------------------------------------

// File: src/projects/web/index.js
import updateProjectUseCase from &amp;#39;src/projects/use-cases/update-project.use-case&amp;#39;;
import { getCurrentUser } from &amp;#39;src/shared/http/helpers&amp;#39;;

app.put(&amp;#39;/project/:projectId&amp;#39;, async ({ request, response }) =&amp;gt; {
  const currentUser = getCurrentUser(request);
  const projectUpdatePayload = /* ... */;
  const projectId = /* ... */;
  const updatedProject = await updateProjectUseCase({ 
    projectId, 
    payload: projectUpdatePayload, 
    actor: currentUser
  });
  /* ... */
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the example there&amp;#39;s a project module containing all related functionality, it is composed of some smaller modules called policies, web, use-cases, and I hint that there could be different files inside them. The point is that to apply conceptual compression at module level, there are many layers of abstraction to deal with and this becomes a new challenge. Having a good balance of abstraction layers is essential.&lt;/p&gt;
&lt;p&gt;Taking for example the `can-update-project.policy`, which main goal is to define when a user can update a project, if this function would have to deal with SQL queries it would&amp;#39;ve made it more complex and harder to understand.&lt;/p&gt;
&lt;h2&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;Navigating the complexities of business logic often leads to intricate coding challenges. Conceptual compression serves as a powerful tool in this regard, helping to simplify how programs are structured by creating effective abstractions, making a better bridge between the problem and its solution, creating that joy in readers. This practice not only enhances code readability but also reduces complexity and promotes maintainability and scalability of your codebase.&lt;/p&gt;
&lt;p&gt;But it also brings big challenges, such as deciding what concepts belong together, or what&amp;#39;s the right connection strength that will break module&amp;#39;s apart. Regardless of that, I argue that those challenges are there anyway in Software Development, applying Conceptual Compression it&amp;#39;s only making them explicit. Feels like a good way to approach Software Design and architectural decisions in general.&lt;/p&gt;
</content:encoded></item><item><title>Writing maintainable tests</title><link>https://karloscodes.com/blog/writing-maintainable-tests/</link><guid isPermaLink="true">https://karloscodes.com/blog/writing-maintainable-tests/</guid><description>Writing maintainable tests</description><pubDate>Wed, 02 Oct 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Alright, so after a decade-plus in Ruby, I&amp;#39;ve gotten into some great debates about testing with colleagues. Through these conversations, I&amp;#39;ve come up with a testing style that&amp;#39;s not exactly textbook, but it&amp;#39;s been doing the job and doing it well. Good to get this down for my own clarity and future reference.&lt;/p&gt;
&lt;h2&gt;Anatomy of my ideal test&lt;/h2&gt;
&lt;p&gt;Here&amp;#39;s a structured blueprint of how I craft my tests:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;it &amp;#39;provides a succinct description of the behavior&amp;#39; do
  setup
  exercise
  verify
  teardown
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Key Characteristics&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verbose&lt;/strong&gt;: Each test is self-explanatory, serving as its documentation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;State isolation&lt;/strong&gt;: Avoid shared global state to prevent entanglement.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Four-step structure&lt;/strong&gt;: The tests follow the &lt;code&gt;Setup, Exercise, Verify, Teardown&lt;/code&gt; paradigm.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Co-location&lt;/strong&gt;: All four steps reside within the same block.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Visual clarity&lt;/strong&gt;: Steps are demarcated with empty lines for easier navigation.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Rationale&lt;/h2&gt;
&lt;h3&gt;Modularity for future changes&lt;/h3&gt;
&lt;p&gt;In my observation, most test modifications are constrained to specific portions of the test file. This isolation eliminates the need to refactor unrelated tests, ensuring that changes are smooth and swift.&lt;/p&gt;
&lt;h3&gt;Avoiding shared state&lt;/h3&gt;
&lt;p&gt;Shared states in tests invite two vices: mutability and coupling. They often lead to complex setups and tear-downs that become increasingly harder to manage as your codebase evolves.&lt;/p&gt;
&lt;h3&gt;Advocacy for verbosity&lt;/h3&gt;
&lt;p&gt;Tests aren&amp;#39;t just tests; they are documentation. A verbose test provides more context, aiding in both readability and maintainability.&lt;/p&gt;
&lt;h3&gt;Explicit test steps&lt;/h3&gt;
&lt;p&gt;Each of the four steps —&lt;code&gt;Setup, Exercise, Verify, Teardown&lt;/code&gt;— is designed to be semantically clear. This separation enhances readability, serving as a reliable guide through complex test scenarios.&lt;/p&gt;
&lt;h2&gt;Concluding remarks&lt;/h2&gt;
&lt;p&gt;While Ruby’s philosophy often emphasizes principles like DRY (Don&amp;#39;t Repeat Yourself), I&amp;#39;ve found that a different mindset is necessary for testing. Duplication is fine in this case in favor of an optimal test case isolation. I feel confident that this testing approach delivers a strong return on investment.&lt;/p&gt;
</content:encoded></item><item><title>System back-pressure</title><link>https://karloscodes.com/blog/system-back-pressure/</link><guid isPermaLink="true">https://karloscodes.com/blog/system-back-pressure/</guid><description>System back-pressure</description><pubDate>Sat, 24 Feb 2018 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;What&amp;#39;s back-pressure&lt;/h2&gt;
&lt;p&gt;Back-pressure happens when data comes in faster than your system can handle it. A slow database can&amp;#39;t keep up with incoming requests, so the HTTP layer starts timing out. One bottleneck breaks everything upstream.&lt;/p&gt;
&lt;p&gt;Understanding back-pressure is undervalued. It&amp;#39;s not just about preventing failures — it&amp;#39;s about adding capacity control mechanisms that let you control data flow speed across your entire system.&lt;/p&gt;
&lt;h2&gt;Back-pressure effects&lt;/h2&gt;
&lt;p&gt;When your database can&amp;#39;t keep up with write requests, queries start piling up. The backlog hits the HTTP layer — requests timeout, errors spike, users complain. The breakdown spreads. One slow component takes down the whole system.&lt;/p&gt;
&lt;h2&gt;Not just a database issue&lt;/h2&gt;
&lt;p&gt;Databases are the usual suspect, but any component can cause back-pressure — a message bus, a cache, an external API. When something downstream slows down, everything upstream breaks.&lt;/p&gt;
&lt;h2&gt;Solutions&lt;/h2&gt;
&lt;p&gt;The obvious fix is to throw more resources at the database. But that&amp;#39;s reactive and expensive.&lt;/p&gt;
&lt;p&gt;A better approach: add a buffer between the HTTP layer and the database. Queue incoming requests and drain them at a pace the database can handle. This decouples request handling from database state — requests don&amp;#39;t fail just because the database is temporarily slow.&lt;/p&gt;
&lt;p&gt;This makes your system more reliable &lt;em&gt;and&lt;/em&gt; cheaper. You&amp;#39;re not paying for capacity you only need during spikes.&lt;/p&gt;
&lt;h2&gt;Pull systems&lt;/h2&gt;
&lt;p&gt;Push systems send data as fast as they can. Pull systems let the consumer request data when it&amp;#39;s ready.&lt;/p&gt;
&lt;p&gt;Pull is underrated. The consumer controls the pace, so it never gets overwhelmed. This adds reliability that push architectures often lack. It also means better resource utilization — you&amp;#39;re not wasting compute on data you can&amp;#39;t process yet.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Back-pressure breaks systems when data floods in faster than you can process it. The fix is simple: buffers or pull systems to control the flow. Prevents crashes. Saves money. Worth keeping in mind before you reach for the &amp;quot;scale up&amp;quot; button.&lt;/p&gt;
</content:encoded></item></channel></rss>