<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged legacy</title>
<link>https://coderlegion.com/tag/legacy</link>
<description>Powered by Question2Answer</description>
<item>
<title>Rewrite or Refactor a Legacy Application? How to Decide</title>
<link>https://coderlegion.com/31357/rewrite-or-refactor-a-legacy-application-how-to-decide</link>
<description>&lt;p&gt;Every business with an ageing application eventually faces the same question: do we &lt;strong&gt;rewrite or refactor the legacy application&lt;/strong&gt;? Get it wrong and you either burn a year rebuilding something that already worked, or you pour money into patching a system that cannot be saved.&lt;/p&gt;
&lt;p&gt;I handle this decision as part of &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/services/legacy-application-modernization&quot;&gt;legacy application modernization&lt;/a&gt; work, and the honest answer is almost never &quot;rewrite everything&quot; or &quot;leave it alone&quot;. This guide gives you a practical way to decide, the questions I ask, and the middle path that usually wins.&lt;/p&gt;
&lt;h3 id=&quot;what-legacy-actually-means&quot;&gt;What &quot;legacy&quot; actually means&lt;/h3&gt;
&lt;p&gt;Old code is not automatically legacy code. An application is legacy when it is &lt;strong&gt;hard to change safely&lt;/strong&gt;. Typical signs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nobody dares to deploy on a Friday, or at all&lt;/li&gt;
&lt;li&gt;Only one person understands how it works&lt;/li&gt;
&lt;li&gt;The framework or language version no longer gets security updates&lt;/li&gt;
&lt;li&gt;Simple changes take weeks because everything is tangled together&lt;/li&gt;
&lt;li&gt;There are no tests, so every change is a gamble&lt;/li&gt;
&lt;li&gt;Hosting and dependencies are frozen because upgrading might break things&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the app is old but stable, tested and easy to change, it is not your problem. If it is new but a mess, it might be.&lt;/p&gt;
&lt;h3 id=&quot;the-three-options-in-plain-terms&quot;&gt;The three options, in plain terms&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Refactor in place.&lt;/strong&gt; Improve the existing code step by step without changing what the system does: add tests, clean up the worst areas, upgrade dependencies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rewrite from scratch.&lt;/strong&gt; Build a new system and switch over when it is ready.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modernise incrementally.&lt;/strong&gt; Replace the system piece by piece while the old one keeps running. This is often called the strangler pattern, described well in &lt;a rel=&quot;nofollow&quot; href=&quot;https://martinfowler.com/bliki/StranglerFigApplication.html&quot;&gt;Martin Fowler's write-up of the Strangler Fig approach&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Most teams think they have two choices. The third is what usually works.&lt;/p&gt;
&lt;h3 id=&quot;why-big-bang-rewrites-fail-so-often&quot;&gt;Why big-bang rewrites fail so often&lt;/h3&gt;
&lt;p&gt;A full rewrite looks attractive: clean code, modern tools, no baggage. The problem is that the old system contains years of quiet business knowledge: edge cases, workarounds, rules nobody wrote down. Joel Spolsky made this argument in his classic essay &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/&quot;&gt;Things You Should Never Do, Part I&lt;/a&gt;, and it still holds up.&lt;/p&gt;
&lt;p&gt;In practice, rewrites go wrong for predictable reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The old system keeps changing&lt;/strong&gt; while you build the new one, so you are chasing a moving target.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You ship nothing for months&lt;/strong&gt;, so the business pays but sees no value.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hidden behaviour is lost&lt;/strong&gt; because nobody remembers why it worked that way.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scope grows&lt;/strong&gt;, because a rewrite invites &quot;while we're at it&quot; features.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That does not mean never rewrite. It means be sure you must, and limit the blast radius.&lt;/p&gt;
&lt;h3 id=&quot;a-decision-framework-you-can-use-this-week&quot;&gt;A decision framework you can use this week&lt;/h3&gt;
&lt;p&gt;Score your system on these questions. I use them in audits.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Can the system still do its job?&lt;/strong&gt; If it works and the business is not blocked, you have time to move carefully. If it is failing daily, you may need to stabilise first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Is the technology end-of-life?&lt;/strong&gt; An unsupported framework or language version is a security and hiring risk. That pushes you toward upgrading or replacing, not ignoring.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Is the data model sound?&lt;/strong&gt; A good database under a messy application is a great position. A broken data model is a much bigger problem than ugly code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Can you test it?&lt;/strong&gt; If you can add tests around key flows, refactoring becomes safe. If the code cannot be tested at all, that is a strong signal to replace parts of it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. How tangled are the pieces?&lt;/strong&gt; Systems with clear boundaries can be replaced one module at a time. A single ball of mud is harder.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. What does the business need next?&lt;/strong&gt; If the next year's roadmap is mostly new features, spend effort where those features land, not on parts nobody touches.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7. Who knows the system?&lt;/strong&gt; If that knowledge is walking out the door, document and test first, whatever you choose.&lt;/p&gt;
&lt;h3 id=&quot;when-refactoring-is-the-right-call&quot;&gt;When refactoring is the right call&lt;/h3&gt;
&lt;p&gt;Choose refactoring when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The framework is still supported or has a clear upgrade path&lt;/li&gt;
&lt;li&gt;The data model is sound&lt;/li&gt;
&lt;li&gt;The pain is concentrated in a few areas&lt;/li&gt;
&lt;li&gt;You can put tests around the critical flows&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I often start with exactly this: map the dependencies, add tests around the riskiest workflows, fix the worst hotspots, and upgrade in steps. My &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/laravel-upgrade-checklist&quot;&gt;Laravel upgrade checklist&lt;/a&gt; walks through that approach for older Laravel apps.&lt;/p&gt;
&lt;h3 id=&quot;when-a-rewrite-is-justified&quot;&gt;When a rewrite is justified&lt;/h3&gt;
&lt;p&gt;A rewrite makes sense when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The platform is end-of-life with no sensible upgrade path&lt;/li&gt;
&lt;li&gt;The architecture fundamentally cannot support what the business needs&lt;/li&gt;
&lt;li&gt;The system is small enough to rebuild in a short, fixed window&lt;/li&gt;
&lt;li&gt;The code is so untestable that incremental change is not possible&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even then, I prefer to rebuild in slices, with the old system running beside the new one.&lt;/p&gt;
&lt;h3 id=&quot;the-incremental-path-most-businesses-should-take&quot;&gt;The incremental path most businesses should take&lt;/h3&gt;
&lt;p&gt;The approach I recommend most often has five steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Audit first.&lt;/strong&gt; Understand the workflows, the database, the dependencies and the risks before deciding anything.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stabilise.&lt;/strong&gt; Fix security gaps, add monitoring and backups, and make deployments repeatable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add tests&lt;/strong&gt; around the flows that matter most.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Replace one slice at a time.&lt;/strong&gt; Pick a self-contained module, rebuild it in the new stack and route traffic to it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retire the old parts&lt;/strong&gt; once the new ones have proven themselves.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each step delivers something the business can see, and each one can be paused or reversed.&lt;/p&gt;
&lt;h3 id=&quot;what-drives-the-cost-either-way&quot;&gt;What drives the cost either way&lt;/h3&gt;
&lt;p&gt;I will not quote a number without seeing the system, but these factors move cost and timeline the most:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Size and complexity&lt;/strong&gt; of the application, especially the number of distinct workflows&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test coverage&lt;/strong&gt;, because untested code costs more to change safely&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data migration&lt;/strong&gt;, since moving and validating years of records is often the riskiest step&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integrations&lt;/strong&gt; with payment, shipping, CRM and other third-party services&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How much downtime the business can tolerate&lt;/strong&gt; during the switch&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation and access&lt;/strong&gt;, because missing knowledge slows everything down&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An audit before the main work is usually the cheapest way to turn these unknowns into a plan you can budget.&lt;/p&gt;
&lt;h3 id=&quot;a-real-world-example&quot;&gt;A real-world example&lt;/h3&gt;
&lt;p&gt;On the &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/work/jamaica-freight-forwarding-core-php-audit&quot;&gt;PPCWA freight-forwarding audit&lt;/a&gt;, I was brought in to audit and stabilise a nearly completed Core PHP and MySQL platform. The work focused on understanding the existing system first: workflows, database structure, frontend maintainability, dependencies and configuration. I also assessed the architectural path toward a future Laravel migration. The point was to decide with evidence instead of opinion. I describe a related migration in &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/migrating-legacy-core-php-platform-to-laravel&quot;&gt;migrating a legacy Core PHP platform to Laravel&lt;/a&gt; and a Laravel-specific version in &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/laravel-5-migration-legacy-application-modernization&quot;&gt;Laravel 5 migration strategy&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;questions-to-ask-any-developer-or-agency&quot;&gt;Questions to ask any developer or agency&lt;/h3&gt;
&lt;p&gt;If someone recommends a rewrite, ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What did you find in the existing system that cannot be improved?&lt;/li&gt;
&lt;li&gt;How will we keep the business running during the work?&lt;/li&gt;
&lt;li&gt;Which parts will go live first, and when?&lt;/li&gt;
&lt;li&gt;How will you protect the data and the undocumented business rules?&lt;/li&gt;
&lt;li&gt;What is the plan if the project overruns?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A good partner answers with specifics. A weak one answers with enthusiasm for new technology.&lt;/p&gt;
&lt;h3 id=&quot;key-takeaways&quot;&gt;Key takeaways&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&quot;Legacy&quot; means hard to change safely, not just old.&lt;/li&gt;
&lt;li&gt;Big-bang rewrites fail because they lose hidden business knowledge and deliver late.&lt;/li&gt;
&lt;li&gt;Audit first, then choose refactor, rewrite or incremental replacement with evidence.&lt;/li&gt;
&lt;li&gt;Incremental modernisation is usually the lowest-risk option.&lt;/li&gt;
&lt;li&gt;A sound data model and testable flows tilt the decision toward refactoring.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you are weighing this decision for your own application, &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/book&quot;&gt;book a call&lt;/a&gt; and I will tell you which path I would take and why.&lt;/p&gt;
&lt;p&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/rewrite-vs-refactor-legacy-application&quot;&gt;aqibjavaid.site&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31357/rewrite-or-refactor-a-legacy-application-how-to-decide</guid>
<pubDate>Fri, 16 Oct 2026 14:55:27 +0000</pubDate>
</item>
<item>
<title>Modernizing Legacy Software</title>
<link>https://coderlegion.com/21266/modernizing-legacy-software</link>
<description>&lt;p&gt;In this episode of Continuous Delivery Office Hours, Tony Kelly chats to Bob Walker and Steve Fenton about modernizing legacy applications.&lt;/p&gt;
&lt;p&gt;Many teams and organizations face the decision on whether to modernize software that's been running for years, and it's no small undertaking.&lt;/p&gt;
&lt;p&gt;Listen to find out more about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Why legacy applications are still hanging around&lt;/li&gt;
&lt;li&gt;The common approach (that's certain to fail)&lt;/li&gt;
&lt;li&gt;What approaches work when you need to update or replace a legacy application&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Videos</category>
<guid isPermaLink="true">https://coderlegion.com/21266/modernizing-legacy-software</guid>
<pubDate>Wed, 26 Aug 2026 09:00:00 +0000</pubDate>
</item>
<item>
<title>Migrating a Legacy Codebase Across 15+ Modules Without Downtime: An Engineering Transformation Story</title>
<link>https://coderlegion.com/14328/migrating-legacy-codebase-across-modules-without-downtime-engineering-transformation</link>
<description>&lt;p&gt;Legacy code is not a dirty word — it is code that makes money, serves real users, and pays engineering salaries. The problem is not its existence, but that it eventually accumulates enough technical debt to slow the entire business down.&lt;/p&gt;
&lt;p&gt;I know this because I have spent the past three years living it. Since September 2022, I have been working as a Full Stack Engineer at VacancySoft, a data intelligence company whose platform serves vacancy data to clients across the recruitment and professional services industries. When I joined, the platform ran on a legacy monolith that had served the company well — but was buckling under 50,000+ daily API requests, growing data volumes, and a feature velocity the architecture could no longer support.&lt;/p&gt;
&lt;p&gt;Over the following months, I led the architectural redesign and incremental migration of 15+ modules to a modern Node.js/TypeScript architecture — without a single minute of planned downtime. This is the story of how we did it: the strategy, the patterns, the code, and the results.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem-a-monolith-under-pressure&quot;&gt;The Problem: A Monolith Under Pressure&lt;/h3&gt;
&lt;p&gt;The platform had the hallmarks of a system that grew organically over many years — not badly built, but built for a different era. By the time I joined, several compounding issues were creating real business impact:&lt;/p&gt;
&lt;h4 id=&quot;technical-debt-symptoms&quot;&gt;Technical Debt Symptoms&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Inconsistent module architecture.&lt;/strong&gt; Each of the 15+ modules had been built by different engineers at different times. Some used callbacks, others early Promises, a few had partial async/await. There was no unified error handling, no consistent validation layer, and no shared utility patterns.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tightly coupled database queries.&lt;/strong&gt; Business logic was interleaved with raw SQL scattered throughout controllers and route handlers. Schema changes required tracing query strings across dozens of files, with no ORM or query builder providing a single source of truth.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No TypeScript, no type safety.&lt;/strong&gt; The entire codebase was plain JavaScript. Refactoring any function required manually tracing every call site to understand expected input and output shapes. Function signatures were documented only in the minds of the original authors — several of whom had left the company.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test coverage below 15%.&lt;/strong&gt; Critical business logic — data transformation, API response formatting, access control — had no automated tests. Every deployment was a manual QA exercise, and regressions were discovered by clients.&lt;/p&gt;
&lt;h4 id=&quot;business-impact&quot;&gt;Business Impact&lt;/h4&gt;
&lt;p&gt;These issues translated directly into business problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Feature delivery had slowed ~40%.&lt;/strong&gt; Engineers spent more time working around existing code than writing new functionality.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Query execution times had degraded.&lt;/strong&gt; Key client-facing endpoints were responding in 2+ seconds, triggering SLA concerns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployment confidence was low.&lt;/strong&gt; Without test coverage, the team batched changes into infrequent releases rather than shipping continuously.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Onboarding new engineers took weeks.&lt;/strong&gt; Missing type definitions, documentation, and consistent patterns meant new team members needed extensive hand-holding.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The codebase needed a fundamental transformation — but the platform was in active use by paying clients. A &quot;big bang&quot; rewrite was commercially unacceptable. We needed to modernise incrementally while keeping the platform fully operational.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;my-role-architect-and-migration-lead&quot;&gt;My Role: Architect and Migration Lead&lt;/h3&gt;
&lt;p&gt;I drove the migration strategy from conception to execution — designing the architectural approach, selecting the migration pattern, defining TypeScript adoption standards, establishing the testing strategy, and personally leading the refactoring of the most complex modules. I also mentored other engineers through the transition, running code reviews to ensure consistency.&lt;/p&gt;
&lt;p&gt;This was not a committee decision. I proposed the strangler fig approach to leadership, built the proof of concept on the first module, and established every pattern the rest of the migration followed.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-strategy-strangler-fig-pattern&quot;&gt;The Strategy: Strangler Fig Pattern&lt;/h3&gt;
&lt;p&gt;I chose the strangler fig pattern — named after the tropical vine that gradually envelops and replaces its host tree. Rather than rewriting everything at once, we would build new implementations alongside legacy code, gradually routing traffic to new modules, and removing old code once each was fully replaced.&lt;/p&gt;
&lt;p&gt;This approach offered three critical advantages:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Zero downtime.&lt;/strong&gt; At no point would the platform be partially functional. Both old and new implementations ran simultaneously.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Incremental risk.&lt;/strong&gt; Each module migration was an isolated, reversible change. If a new module had issues, we could route traffic back to the legacy implementation within seconds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Continuous feature delivery.&lt;/strong&gt; The team could ship new features on modules that had not yet been migrated, while migration work progressed in parallel on other modules.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;the-migration-architecture&quot;&gt;The Migration Architecture&lt;/h4&gt;
&lt;p&gt;I designed a proxy layer sitting in front of both legacy and new implementations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// migration-router.ts — Proxy layer for gradual migration
import { Request, Response, NextFunction } from 'express';
import { FeatureFlagService } from './services/feature-flags';

interface MigrationRoute {
  legacyHandler: (req: Request, res: Response, next: NextFunction) =&amp;gt; void;
  modernHandler: (req: Request, res: Response, next: NextFunction) =&amp;gt; void;
  moduleName: string;
}

export function createMigrationRouter(route: MigrationRoute) {
  return async (req: Request, res: Response, next: NextFunction) =&amp;gt; {
    const useModern = await FeatureFlagService.isEnabled(
      `migration:${route.moduleName}`,
      { userId: req.user?.id }
    );

    if (useModern) {
      return route.modernHandler(req, res, next);
    }

    return route.legacyHandler(req, res, next);
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Feature flags controlled which implementation served each request. I could migrate internal users first, then roll out to 10%, 25%, 50%, and finally 100% of client traffic — monitoring error rates and response times at every stage.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;technical-deep-dive-the-migration-process&quot;&gt;Technical Deep Dive: The Migration Process&lt;/h3&gt;
&lt;h4 id=&quot;phase-1-typescript-foundation-and-shared-infrastructure&quot;&gt;Phase 1: TypeScript Foundation and Shared Infrastructure&lt;/h4&gt;
&lt;p&gt;Before migrating any module, I established the shared infrastructure every new module would depend on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TypeScript Configuration&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I introduced TypeScript with a strict configuration enforcing type safety from the start:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;ES2020&quot;,
    &quot;module&quot;: &quot;commonjs&quot;,
    &quot;strict&quot;: true,
    &quot;noImplicitAny&quot;: true,
    &quot;strictNullChecks&quot;: true,
    &quot;esModuleInterop&quot;: true,
    &quot;sourceMap&quot;: true,
    &quot;outDir&quot;: &quot;./dist&quot;,
    &quot;rootDir&quot;: &quot;./src&quot;,
    &quot;paths&quot;: {
      &quot;@modules/*&quot;: [&quot;src/modules/*&quot;],
      &quot;@shared/*&quot;: [&quot;src/shared/*&quot;]
    }
  },
  &quot;include&quot;: [&quot;src/**/*&quot;],
  &quot;exclude&quot;: [&quot;node_modules&quot;, &quot;dist&quot;, &quot;legacy/**/*&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;strict: true&lt;/code&gt; flag was non-negotiable. I had seen migrations where &lt;code&gt;strict: false&lt;/code&gt; was used to avoid friction, yielding TypeScript no safer than the JavaScript it replaced.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shared Data Access Layer&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I built a unified data access layer replacing scattered raw SQL queries with typed repository patterns:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// shared/repositories/base.repository.ts
import { Pool, QueryResult } from 'pg';
import { DatabasePool } from '../database/pool';

export abstract class BaseRepository&amp;lt;T&amp;gt; {
  protected pool: Pool;

  constructor() {
    this.pool = DatabasePool.getInstance();
  }

  protected async query&amp;lt;R = T&amp;gt;(
    sql: string,
    params: unknown[] = []
  ): Promise&amp;lt;R[]&amp;gt; {
    const start = Date.now();
    const result: QueryResult = await this.pool.query(sql, params);
    const duration = Date.now() - start;

    if (duration &amp;gt; 500) {
      logger.warn('Slow query detected', {
        sql: sql.substring(0, 200),
        duration,
        rowCount: result.rowCount,
      });
    }

    return result.rows as R[];
  }

  protected async queryOne&amp;lt;R = T&amp;gt;(
    sql: string,
    params: unknown[] = []
  ): Promise&amp;lt;R | null&amp;gt; {
    const rows = await this.query&amp;lt;R&amp;gt;(sql, params);
    return rows[0] || null;
  }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every migrated module used this base repository, giving us automatic slow query logging, consistent error handling, and typed return values. The legacy code had none of this — slow queries went undetected until clients complained.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Standardised Error Handling&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I introduced a typed error hierarchy replacing the inconsistent error handling across the legacy codebase:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// shared/errors/application-errors.ts
export class ApplicationError extends Error {
  constructor(
    message: string,
    public readonly statusCode: number,
    public readonly code: string,
    public readonly isOperational: boolean = true
  ) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

export class NotFoundError extends ApplicationError {
  constructor(resource: string, identifier: string | number) {
    super(
      `${resource} with identifier '${identifier}' not found`,
      404,
      'RESOURCE_NOT_FOUND'
    );
  }
}

export class ValidationError extends ApplicationError {
  constructor(
    public readonly errors: Array&amp;lt;{ field: string; message: string }&amp;gt;
  ) {
    super('Validation failed', 400, 'VALIDATION_ERROR');
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;phase-2-module-by-module-migration&quot;&gt;Phase 2: Module-by-Module Migration&lt;/h4&gt;
&lt;p&gt;With the foundation in place, I began migrating modules from lowest-risk to highest-risk — starting with lower-traffic, simpler modules to build confidence before tackling the complex, high-traffic ones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before (Legacy JavaScript):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// legacy/controllers/vacancyController.js
const db = require('../db');

exports.getVacancies = function(req, res) {
  var page = req.query.page || 1;
  var limit = req.query.limit || 50;
  var offset = (page - 1) * limit;
  var sector = req.query.sector;

  var sql = 'SELECT * FROM vacancies';
  var params = [];

  if (sector) {
    sql += ' WHERE sector = $1';
    params.push(sector);
  }

  sql += ' ORDER BY created_at DESC LIMIT $' + (params.length + 1) +
         ' OFFSET $' + (params.length + 2);
  params.push(limit, offset);

  db.query(sql, params, function(err, result) {
    if (err) {
      console.log('Error fetching vacancies:', err);
      return res.status(500).json({ error: 'Internal server error' });
    }

    db.query('SELECT COUNT(*) FROM vacancies' +
      (sector ? ' WHERE sector = $1' : ''), sector ? [sector] : [],
      function(err2, countResult) {
        if (err2) {
          return res.status(500).json({ error: 'Internal server error' });
        }
        res.json({
          data: result.rows,
          total: parseInt(countResult.rows[0].count),
          page: parseInt(page),
          limit: parseInt(limit)
        });
      }
    );
  });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;After (Modern TypeScript):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// modules/vacancies/vacancy.controller.ts
import { Request, Response } from 'express';
import { VacancyService } from './vacancy.service';
import { GetVacanciesSchema } from './vacancy.validation';
import { asyncHandler } from '@shared/middleware/async-handler';

export class VacancyController {
  constructor(private readonly vacancyService: VacancyService) {}

  getVacancies = asyncHandler(async (req: Request, res: Response) =&amp;gt; {
    const query = GetVacanciesSchema.parse(req.query);

    const result = await this.vacancyService.getVacancies({
      page: query.page,
      limit: query.limit,
      sector: query.sector,
      sortBy: query.sortBy,
      sortOrder: query.sortOrder,
    });

    res.json({
      data: result.items,
      meta: {
        total: result.total,
        page: result.page,
        limit: result.limit,
        totalPages: Math.ceil(result.total / result.limit),
      },
    });
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// modules/vacancies/vacancy.service.ts
import { VacancyRepository } from './vacancy.repository';
import { CacheService } from '@shared/services/cache.service';
import { PaginatedResult, VacancyListItem } from './vacancy.types';

export class VacancyService {
  constructor(
    private readonly repository: VacancyRepository,
    private readonly cache: CacheService
  ) {}

  async getVacancies(params: GetVacanciesParams): Promise&amp;lt;PaginatedResult&amp;lt;VacancyListItem&amp;gt;&amp;gt; {
    const cacheKey = `vacancies:${JSON.stringify(params)}`;

    const cached = await this.cache.get&amp;lt;PaginatedResult&amp;lt;VacancyListItem&amp;gt;&amp;gt;(cacheKey);
    if (cached) return cached;

    const [items, total] = await Promise.all([
      this.repository.findPaginated(params),
      this.repository.countFiltered(params),
    ]);

    const result: PaginatedResult&amp;lt;VacancyListItem&amp;gt; = {
      items,
      total,
      page: params.page,
      limit: params.limit,
    };

    await this.cache.set(cacheKey, result, 300);
    return result;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The contrast is stark. The legacy version had no validation, no caching, nested callbacks, string-concatenated SQL, and &lt;code&gt;console.log&lt;/code&gt; as its only error reporting. The migrated version has Zod schema validation, a service layer with caching, typed repositories, and structured error handling — all running through the same API routes, invisible to clients.&lt;/p&gt;
&lt;h4 id=&quot;phase-3-query-optimisation-during-migration&quot;&gt;Phase 3: Query Optimisation During Migration&lt;/h4&gt;
&lt;p&gt;Migration was not just code restructuring. I used each module migration as an opportunity to optimise underlying queries, auditing every SQL statement with &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; to identify inefficiencies.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// modules/vacancies/vacancy.repository.ts
import { BaseRepository } from '@shared/repositories/base.repository';
import { VacancyListItem, GetVacanciesParams } from './vacancy.types';

export class VacancyRepository extends BaseRepository&amp;lt;VacancyListItem&amp;gt; {
  async findPaginated(params: GetVacanciesParams): Promise&amp;lt;VacancyListItem[]&amp;gt; {
    const conditions: string[] = [];
    const values: unknown[] = [];
    let paramIndex = 1;

    if (params.sector) {
      conditions.push(`v.sector = $${paramIndex++}`);
      values.push(params.sector);
    }

    const whereClause = conditions.length
      ? `WHERE ${conditions.join(' AND ')}`
      : '';

    values.push(params.limit, (params.page - 1) * params.limit);

    return this.query&amp;lt;VacancyListItem&amp;gt;(`
      SELECT v.id, v.title, v.sector, v.location, v.published_at,
             c.name AS company_name
      FROM vacancies v
      INNER JOIN companies c ON c.id = v.company_id
      ${whereClause}
      ORDER BY v.published_at DESC
      LIMIT \({paramIndex++} OFFSET \){paramIndex}
    `, values);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The legacy code selected &lt;code&gt;SELECT *&lt;/code&gt; from tables with 30+ columns when the API response only needed 6 fields. The migrated queries selected only required columns, used proper indexing, and parallelised count queries with data queries using &lt;code&gt;Promise.all&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;phase-4-testing-strategy&quot;&gt;Phase 4: Testing Strategy&lt;/h4&gt;
&lt;p&gt;Every migrated module shipped with comprehensive tests. I established a three-layer testing strategy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// modules/vacancies/__tests__/vacancy.service.test.ts
describe('VacancyService', () =&amp;gt; {
  let service: VacancyService;
  let mockRepository: jest.Mocked&amp;lt;VacancyRepository&amp;gt;;
  let mockCache: jest.Mocked&amp;lt;CacheService&amp;gt;;

  beforeEach(() =&amp;gt; {
    mockRepository = { findPaginated: jest.fn(), countFiltered: jest.fn() } as any;
    mockCache = { get: jest.fn().mockResolvedValue(null), set: jest.fn() } as any;
    service = new VacancyService(mockRepository, mockCache);
  });

  it('should return cached result when available', async () =&amp;gt; {
    const cached = { items: [{ id: 1, title: 'Engineer' }], total: 1, page: 1, limit: 50 };
    mockCache.get.mockResolvedValue(cached);

    const result = await service.getVacancies({ page: 1, limit: 50 });
    expect(result).toEqual(cached);
    expect(mockRepository.findPaginated).not.toHaveBeenCalled();
  });

  it('should query repository and cache on miss', async () =&amp;gt; {
    mockRepository.findPaginated.mockResolvedValue([{ id: 1, title: 'Engineer' }]);
    mockRepository.countFiltered.mockResolvedValue(1);

    const result = await service.getVacancies({ page: 1, limit: 50 });
    expect(result.items).toHaveLength(1);
    expect(mockCache.set).toHaveBeenCalled();
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Test coverage across migrated modules went from under 15% to above 85%. This fundamentally changed deployment confidence — the team began shipping smaller, more frequent releases because they trusted the test suite to catch regressions.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-results-measured-in-production&quot;&gt;The Results: Measured in Production&lt;/h3&gt;
&lt;p&gt;The migration was executed over a continuous period, with each module validated and fully deployed before moving to the next. Here are the production metrics we measured:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Metric &lt;/th&gt;&lt;th&gt;Before Migration &lt;/th&gt;&lt;th&gt;After Migration &lt;/th&gt;&lt;th&gt;Change&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Average API response time &lt;/td&gt;&lt;td&gt;1.8s &lt;/td&gt;&lt;td&gt;320ms &lt;/td&gt;&lt;td&gt;&lt;strong&gt;82% reduction&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Average query execution time &lt;/td&gt;&lt;td&gt;1.2s &lt;/td&gt;&lt;td&gt;480ms &lt;/td&gt;&lt;td&gt;&lt;strong&gt;60% reduction&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Overall system performance &lt;/td&gt;&lt;td&gt;Baseline &lt;/td&gt;&lt;td&gt;+40% throughput &lt;/td&gt;&lt;td&gt;&lt;strong&gt;40% improvement&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Daily API request capacity &lt;/td&gt;&lt;td&gt;~50,000 (strained) &lt;/td&gt;&lt;td&gt;50,000+ (comfortable headroom) &lt;/td&gt;&lt;td&gt;Stable at scale&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Test coverage &lt;/td&gt;&lt;td&gt;&amp;lt;15% &lt;/td&gt;&lt;td&gt;&amp;gt;85% &lt;/td&gt;&lt;td&gt;&lt;strong&gt;5.7x increase&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Deployment frequency &lt;/td&gt;&lt;td&gt;Bi-weekly &lt;/td&gt;&lt;td&gt;Multiple per week &lt;/td&gt;&lt;td&gt;&lt;strong&gt;3-4x increase&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Production incidents (monthly) &lt;/td&gt;&lt;td&gt;6-8 &lt;/td&gt;&lt;td&gt;1-2 &lt;/td&gt;&lt;td&gt;&lt;strong&gt;75% reduction&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;New engineer onboarding time &lt;/td&gt;&lt;td&gt;~4 weeks &lt;/td&gt;&lt;td&gt;~1.5 weeks &lt;/td&gt;&lt;td&gt;&lt;strong&gt;63% reduction&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Planned downtime during migration &lt;/td&gt;&lt;td&gt;— &lt;/td&gt;&lt;td&gt;&lt;strong&gt;Zero&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;No service interruption&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The 40% performance improvement and 60% query time reduction were measured across the full platform using APM over a 30-day window after the final module was migrated.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;lessons-for-engineers-facing-legacy-migrations&quot;&gt;Lessons for Engineers Facing Legacy Migrations&lt;/h3&gt;
&lt;p&gt;Here is what I would tell any engineer facing a similar challenge:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Never rewrite from scratch.&lt;/strong&gt; The strangler fig pattern works. It feels slower than a rewrite, but is dramatically less risky. The business never stops.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Establish patterns before scaling.&lt;/strong&gt; I spent two weeks building shared infrastructure and migrating one module as a reference. Every subsequent migration was faster because the patterns were proven.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use the migration as a performance audit.&lt;/strong&gt; Every legacy module contains hidden performance debt. Migrating is the perfect time to resolve it — you are already reading every line of code.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Type safety pays for itself immediately.&lt;/strong&gt; The bugs caught by TypeScript during migration — latent in the JavaScript codebase for months — convinced even the most sceptical team members that strict typing was worth it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test coverage makes everything else possible.&lt;/strong&gt; Without tests, every migration step is a gamble. With them, it is engineering.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Measure before, during, and after.&lt;/strong&gt; Without performance numbers, the migration is a story. With them, it is evidence.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;Migrating a legacy codebase is not glamorous work — no greenfield architectures to design, no new frameworks to evaluate. It is the painstaking work of understanding what exists, designing better versions, and replacing components one by one without breaking anything.&lt;/p&gt;
&lt;p&gt;But it is some of the most impactful work an engineer can do. The VacancySoft platform now processes 50,000+ daily requests on an architecture that is maintainable, testable, performant, and ready for growth. The team ships faster, with more confidence, and new engineers become productive in days rather than weeks.&lt;/p&gt;
&lt;p&gt;I have led this transformation from start to finish, and the lessons have shaped how I approach every codebase I work on — legacy or otherwise.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14328/migrating-legacy-codebase-across-modules-without-downtime-engineering-transformation</guid>
<pubDate>Thu, 09 Apr 2026 10:50:00 +0000</pubDate>
</item>
<item>
<title>What legacy codebases taught me about real software engineering.</title>
<link>https://coderlegion.com/11484/what-legacy-codebases-taught-me-about-real-software-engineering</link>
<description>&lt;p&gt;Almost every developer must've worked on at least one legacy project in their career.&lt;br&gt;
And the first reaction is almost always the same. &lt;br&gt;
You open the codebase and think,&lt;br&gt;
&quot;Who wrote this code?&quot;&lt;br&gt;
&quot;Why is this such a mess?&quot;&lt;br&gt;
&quot;I can do a lot better than this&quot;&lt;br&gt;
... something like this.&lt;br&gt;
I too thought the same. And this reaction is justified. Because the first look is rarely pleasant. Confusing structure, outdated patterns, and code that feels impossible to reason about.&lt;br&gt;
When I was first assigned to a legacy project, I wasn't excited at all.&lt;br&gt;
It wasn't new, it wasn't shiny, and it definitely wasn't something I could show off. The codebase was large, the logic was spread across files, and even a small change felt risky. I kept wondering how anyone managed to build and maintain something like this.&lt;br&gt;
It was a scary experience. For every small change, I've spent most of the time navigating the code than actually writing it. A lot of syntax actually felt unfamiliar to me. There was no package managers, no third party libraries, no proper documentation. Everything was written from scratch.&lt;br&gt;
For a modern programmer like me, who uses third party libraries and packages for everything like it is normal, it definitely was a scary experience.&lt;br&gt;
At first, everything felt difficult. Progress was slow and mistakes felt expensive. At some point, I even wondered, why aren't we migrating it to some new and shiny tech.&lt;br&gt;
But as I spent more time navigating the codebase, reading, tracing, and understanding the processes, it started getting easier. I no longer felt lost every time I opened a file. I know where to look, which parts were fragile, and which ones could be changed safely. Debugging became faster, changes became more deliberate, and the fear of breaking things slowly faded away.&lt;br&gt;
Working on legacy projects taught me many things. I no longer solely rely on third party libraries. I was realized package managers and libraries are good to have, but not necessarily needed. I got to know how systems actually work on ground level.&lt;br&gt;
As I continued working on, I've started to admire the developers that built those systems decades ago, from scratch without any of the modern resources available to them. At first, I thought that the legacy developers only wrote messy code, but as time passes, I've started to appreciate the fact that they did their very best with the limited resources available to them amid tight deadlines and the effort they put into building everything from scratch.&lt;br&gt;
Working on legacy projects made me a better programmer than I can imagine. It gave me the confidence to take up new challenges and work on new things. I am no longer afraid of working on things I don't know, because I am confident that I can learn.&lt;br&gt;
Whenever I am assigned to legacy projects, I've started to look at them not as obstacles, but opportunities. Opportunities to learn more about complex systems and get used to the code of programming languages.&lt;br&gt;
I no longer advise on migrating legacy projects into new stack. In fact, I advise against it. Because I believe that legacy projects are more stable than most of the project that are developed with modern tech stack.&lt;br&gt;
For everyone who are working on legacy projects, I want to share some things from my experience.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Legacy projects are not obstacles for your career, but great opportunities for you to learn and grow.&lt;/li&gt;
&lt;li&gt;Legacy projects are not outdated. They are just simple, boring and stable.&lt;/li&gt;
&lt;li&gt;Legacy projects often lack good documentation, so when you are navigating through code, make sure to take notes properly. So that it may help you in the future or others who work on it.&lt;/li&gt;
&lt;li&gt;When you came across tasks that couldn't be done using legacy stack, try to bridge that gap using modern stack. But don't actually try to migrate entire working system into modern stack, just because it's outdated.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Conclusion :&lt;/strong&gt;&lt;br&gt;
Legacy codebases changed the way I look at software engineering.&lt;br&gt;
They taught me that good software isn't always clean or modern, but it's software that continues to deliver value overtime. More importantly, working on legacy stacks helped me become a better programmer and a better architect.&lt;br&gt;
Understanding, maintaining, and improving systems that already exist is deeply rewarding. That's why, today, I genuinely enjoy working with legacy code.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/11484/what-legacy-codebases-taught-me-about-real-software-engineering</guid>
<pubDate>Thu, 12 Feb 2026 09:31:53 +0000</pubDate>
</item>
<item>
<title>JuggleBee’s Great Leap – Data Migration, ActiveStorage, and Production Readiness (Part 2)</title>
<link>https://coderlegion.com/4934/jugglebees-great-leap-data-migration-activestorage-and-production-readiness-part-2</link>
<description>&lt;p&gt;Now that we had a deployable app, I set up a staging server using my new favourite provider, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.hetzner.com/&quot;&gt;Hetzner&lt;/a&gt; (&lt;em&gt;not a sponsor, lol&lt;/em&gt;), spun it up, and witnessed a “fully functional” skeleton of JuggleBee. Stage 1 of the migration plan was complete.&lt;/p&gt;
&lt;p&gt;Now came the real meat and potatoes of the project — actually hydrating the new app with all of its production data. We’re talking over 50,000 images, countless database records, and security credentials… the works. This is the part where a missed step could mean broken listings, missing files, or downtime. No pressure.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;In case you missed &lt;strong&gt;Part 1&lt;/strong&gt;, we covered:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Why I started fresh on &lt;strong&gt;Rails 8&lt;/strong&gt; instead of doing incremental upgrades.&lt;/li&gt;
&lt;li&gt;Migrating core &lt;strong&gt;models/controllers/services&lt;/strong&gt; to modern Rails conventions.&lt;/li&gt;
&lt;li&gt;Swapping Sprockets for &lt;strong&gt;importmaps&lt;/strong&gt; and modernizing the JavaScript setup.&lt;/li&gt;
&lt;li&gt;Replacing &lt;strong&gt;Sidekiq + Redis + whenever&lt;/strong&gt; with &lt;strong&gt;ActiveJob + SolidQueue&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Moving deployments to &lt;strong&gt;Kamal 2&lt;/strong&gt; with Traefik and Let’s Encrypt.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Catch up on it here:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.brazenbraden.com/posts/jugglebee_rails_upgrade_part1/&quot;&gt;JuggleBee’s Great Leap – Rebuilding a Rails 4 App in Rails 8 (Part 1)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now—back to the post.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-daunting-database-migration&quot;&gt;The Daunting Database Migration&lt;/h3&gt;
&lt;p&gt;Legacy JuggleBee had been coasting along happily on Postgres 9.6 — which, back in 2016, was the latest and greatest. Fast forward to today and it’s long past its sell-by date, with official support ending in November 2021. Upgrading wasn’t just about keeping up with the times; Postgres 17.5 brings some serious perks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Native JSON field type support.&lt;/li&gt;
&lt;li&gt;Incremental backups with &lt;code&gt;pg_basebackup --incremental&lt;/code&gt;, slashing backup time and storage usage.&lt;/li&gt;
&lt;li&gt;Noticeable performance gains from better memory management, indexing, and partitioning.&lt;/li&gt;
&lt;li&gt;Improved observability via enhanced &lt;code&gt;EXPLAIN&lt;/code&gt; options and richer stats views.&lt;/li&gt;
&lt;li&gt;…and more.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It was a no-brainer to upgrade, but the version gap meant one wrong move could corrupt thousands of user, auction, and invoice records. That made reliable backups non-negotiable before touching a single byte. Luckily, I already had a manual backup process baked into my deploy routine. Here’s the script (&lt;em&gt;feel free to steal any of the scripts in my posts&lt;/em&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash

DB_CONTAINER=($(sudo docker ps | grep postgres | awk '{print $NF}'))
BACKUP_NAME=dump_`date +%d-%m-%Y&quot;_&quot;%H_%M_%S`.sql
BACKUP_FOLDER=db_backups
KEEP=10

# Dump the database into a backup sql file
echo &quot;Creating the database dump...&quot;
docker exec -t $DB_CONTAINER pg_dumpall -c -U postgres &amp;gt; $BACKUP_NAME
echo &quot;Database dump created.&quot;

echo &quot;Moving database backup to backup folder...&quot;
mkdir -p $BACKUP_FOLDER
mv $BACKUP_NAME $BACKUP_FOLDER
echo &quot;Backup moved to backup folder.&quot;

echo &quot;Deleting old releases...&quot;
(cd $BACKUP_FOLDER &amp;amp;&amp;amp; (ls -t | head -n $KEEP; ls) | sort | uniq -u | xargs rm -rf)
echo &quot;Backup stored.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One catch: &lt;code&gt;pg_dumpall&lt;/code&gt; (used above) tries to dump &lt;em&gt;everything&lt;/em&gt; — including Postgres cluster-wide settings — which can be version-specific and cause headaches on import. For a major version jump, you want &lt;code&gt;pg_dump&lt;/code&gt;, which targets a single database and avoids the version mismatch landmines.&lt;/p&gt;
&lt;p&gt;For the migration, I only needed to change the &lt;code&gt;docker&lt;/code&gt; command we executed - with the added ENV vars, that is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;DB_USER=db_user
DB_PASSWORD=db_password
DATABASE_NAME=database_name

# ...

docker exec -e PGPASSWORD=$DB_PASSWORD -t $DB_CONTAINER \
    pg_dump -U $DB_USER $DATABASE_NAME &amp;gt; $BACKUP_NAME
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the new backup in hand, importing into Postgres 17.5 went off without a hitch — no edits, no drama. Honestly, I was braced for a fight given the version gap, but the data was simple enough (and avoided fancy Postgres features) that it just… worked. A rare and pleasant surprise in a project full of moving parts.&lt;/p&gt;
&lt;h3 id=&quot;listing-image-migration&quot;&gt;Listing Image Migration&lt;/h3&gt;
&lt;p&gt;Unlike the data migration before it, migrating all the listing images (just shy of 50,000!) came with its own set of headaches. The legacy setup used CarrierWave with a custom uploader and fixed S3 paths, which worked fine at the time but wasn’t exactly future-proof. Since I was already upgrading to Rails 8, it made sense to tackle this technical debt head-on and move to ActiveStorage — effectively retiring these gems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;carrierwave&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fog-aws&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mini_magick&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;… and replacing them with &lt;code&gt;image_processing&lt;/code&gt; for post-processing.&lt;/p&gt;
&lt;p&gt;In the old days, uploads required creating an “Uploader” class and mounting it on a model. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# app/models/image.rb
class Image &amp;lt; ActiveRecord::Base
  mount_uploader :aws, ImageUploader
end

# app/uploaders/image_uploader.rb
class ImageUploader &amp;lt; CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick
  storage :fog

  process resize_to_fill: [850, 850]

  version :thumb do
    process resize_to_fill: [280, 280]
  end

  def store_dir
    &quot;listings/#{model.listing_id}/#{model.id}&quot;
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With ActiveStorage, uploaders and hard-coded S3 paths are gone. You simply declare the attachment in your model (after setting up &lt;code&gt;config/storage.yml&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class Image &amp;lt; ApplicationRecord
  has_one_attached :file do |attachable|
    attachable.variant :thumb, resize_to_limit: THUMB_DIMENSIONS, preprocessed: true
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That takes care of &lt;em&gt;future&lt;/em&gt; uploads — but what about migrating the 50,000+ legacy images stored in the old bucket? The &lt;code&gt;image.aws&lt;/code&gt; field plus CarrierWave’s &lt;code&gt;store_dir&lt;/code&gt; gave me the exact file path to each image. I then wrote a script to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Download each image from the legacy bucket.&lt;/li&gt;
&lt;li&gt;Re-upload it through ActiveStorage so it generates its own hashed storage path.&lt;/li&gt;
&lt;li&gt;Skip already-migrated images to allow safe re-runs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here’s the script in all its glory (run synchronously to avoid AWS rate limits — which meant a cool 4 hours of migration time):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;#!/usr/bin/env ruby

require_relative '../config/environment'
require 'aws-sdk-s3'
require 'stringio'

OLD_S3_BUCKET = Rails.application.credentials.dig(:old_aws, :bucket)
OLD_S3_REGION = Rails.application.credentials.dig(:old_aws, :region)
OLD_S3_ACCESS_KEY_ID = Rails.application.credentials.dig(:old_aws, :s3_key)
OLD_S3_SECRET_ACCESS_KEY = Rails.application.credentials.dig(:old_aws, :s3_secret)

# Optional: skip already migrated images
SKIP_EXISTING = true

s3 = Aws::S3::Client.new(
  region: OLD_S3_REGION,
  access_key_id: OLD_S3_ACCESS_KEY_ID,
  secret_access_key: OLD_S3_SECRET_ACCESS_KEY
)

puts &quot;Starting image migration...&quot;

Image.find_each do |image|
  begin
    if SKIP_EXISTING &amp;amp;&amp;amp; image.file.attached?
      puts &quot;Skipping Image #{image.id} (already attached)&quot;
      next
    end

    # Construct old S3 key from uploader logic - your path here
    key = &quot;listings/#{image.listing_id}/#{image.id}/#{File.basename(image[:aws].to_s)}&quot;

    obj = s3.get_object(bucket: OLD_S3_BUCKET, key: key)

    image.file.attach(
      io: StringIO.new(obj.body.read),
      filename: File.basename(key),
      content_type: obj.content_type || 'image/jpeg'
    )

    puts &quot;Migrated Image #{image.id} (Listing #{image.listing_id})&quot;
  rescue Aws::S3::Errors::NoSuchKey =&amp;gt; e
    warn &quot;Image #{image.id} missing in old bucket: #{key}&quot;
  rescue =&amp;gt; e
    warn &quot;Failed to migrate Image #{image.id}: #{e.class} - #{e.message}&quot;
  end
end

puts &quot;Migration script finished&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four hours later, we were in business — all images were now managed by ActiveStorage.&lt;br&gt;
Future uploads are handled automatically, thumbnails are generated on demand, and the whole setup is cleaner and easier to maintain.&lt;/p&gt;
&lt;h3 id=&quot;credentials-amp-security&quot;&gt;Credentials &amp;amp; Security&lt;/h3&gt;
&lt;p&gt;You might have noticed the use of &lt;code&gt;Rails.application.credentials&lt;/code&gt; in the previous script. That’s new territory — Rails encrypted credentials didn’t exist back in 4.2.&lt;/p&gt;
&lt;p&gt;Back then we had &lt;code&gt;secrets.yml&lt;/code&gt;, which stored everything in plain text and was meant to be excluded from Git. In theory, you’d inject it during deploy with whatever tooling you used. In practice… I was lazy. My deploys were manual, the repo was private, and I just checked them straight in. &lt;strong&gt;Big no-no.&lt;/strong&gt; Thankfully, Bitbucket never got hacked — but let’s just say this setup wouldn’t pass any kind of security audit.&lt;/p&gt;
&lt;p&gt;Rails 5.2 introduced encrypted credentials — finally, a way to store secrets in plain text &lt;em&gt;locally&lt;/em&gt; while Rails encrypts them with a master key. That master key stays out of Git and gets injected during deploy. Much better.&lt;/p&gt;
&lt;p&gt;Fast forward to today, and Kamal (my de facto deployment tool) takes it a step further with &lt;a rel=&quot;nofollow&quot; href=&quot;https://kamal-deploy.org/docs/commands/secrets/&quot;&gt;built-in support for pulling secrets&lt;/a&gt; straight from password managers like Bitwarden, 1Password, or LastPass. That means my Rails master key, database credentials, and other environment variables can live safely in Bitwarden, and Kamal just grabs them during deploy. No manual copying, no “where did I put that key?” moments, and no plain-text secrets hanging around in the repo.&lt;/p&gt;
&lt;p&gt;Here’s an example using Bitwarden via the CLI. This snippet fetches all secrets and assigns them to variables in &lt;code&gt;.kamal/secrets&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;SECRETS=$(kamal secrets fetch --adapter bitwarden-sm all)

KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS)
RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
POSTGRES_USER=$(kamal secrets extract POSTGRES_USER $SECRETS)
POSTGRES_PASSWORD=$(kamal secrets extract POSTGRES_PASSWORD $SECRETS)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The only catch: you’ll need to have the Bitwarden CLI installed and authenticated on every machine you deploy from. It’s a one-time setup, but totally worth it for the peace of mind of never having to worry about leaked secrets again.&lt;/p&gt;
&lt;h3 id=&quot;lessons-learned&quot;&gt;Lessons Learned&lt;/h3&gt;
&lt;p&gt;Looking back, a few big takeaways stood out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scripts are your best friend.&lt;/strong&gt; Whether it’s a database dump, image migration, or credentials setup, having a repeatable script means you can run it, tweak it, and run it again without reinventing the wheel. It also removes a ton of mental overhead when you’re juggling multiple moving parts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test migrations in staging — thoroughly.&lt;/strong&gt; Moving to PostgreSQL 17.5 could have been a disaster if I’d gone straight into production. Running the full migration end-to-end in a safe environment gave me confidence and caught subtle issues before they ever touched live data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Expect the cleanup phase.&lt;/strong&gt; Even after the “big ticket” migrations were done, staging revealed a laundry list of smaller fixes — broken image links, JavaScript load order issues, outdated gem calls, and other gremlins. None of these were major individually, but together they took nearly two weeks to iron out. Build that buffer into your timeline.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Take the opportunity to ditch technical debt.&lt;/strong&gt; CarrierWave, fog-aws, Sidekiq, Redis… all of these had served me well for years, but replacing them with ActiveStorage and SolidQueue means fewer dependencies to maintain, fewer services to monitor, and a cleaner architecture.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Security deserves equal billing with features.&lt;/strong&gt; Rails encrypted credentials plus Bitwarden CLI integration finally dragged my secrets management into the modern age. No more plain-text secrets in Git. Enough said.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Modern Rails is worth the leap.&lt;/strong&gt; From importmaps to Kamal, the tooling around Rails 8 makes deployment, scaling, and maintenance dramatically simpler than what I was doing in 2015.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;closing-thoughts&quot;&gt;Closing thoughts&lt;/h3&gt;
&lt;p&gt;And with that, JuggleBee officially joined the modern Rails era.&lt;/p&gt;
&lt;p&gt;This wasn’t just a version bump — it was a full-stack rejuvenation.&lt;br&gt;
The app runs faster, the infrastructure is leaner, and I finally have a deployment pipeline I can trust at 2 a.m. without crossing my fingers.&lt;/p&gt;
&lt;p&gt;Was it worth skipping incremental upgrades? Absolutely. Starting fresh on Rails 8 gave me a clean slate to rebuild only what mattered, using tools that will keep JuggleBee humming for years to come.&lt;/p&gt;
&lt;p&gt;If you’ve been sitting on a legacy Rails app, staring down the upgrade path and wondering if it’s worth the effort — it is. Just plan your migrations, test them in staging, and embrace the opportunity to shed some old weight while you’re at it.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.jugglebee.com&quot;&gt;JuggleBee’s&lt;/a&gt; now live on &lt;strong&gt;Rails 8 / Ruby 3.4.3 / PostgreSQL 17.5&lt;/strong&gt;, with ActiveStorage handling thousands of images, Bitwarden keeping our secrets safe, and Kamal making deployments a one-command affair.&lt;/p&gt;
&lt;p&gt;Now it’s time to step away from the terminal, grab a cold beer, and actually enjoy the thing I’ve been rebuilding over the last month — until the next big upgrade comes along.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/4934/jugglebees-great-leap-data-migration-activestorage-and-production-readiness-part-2</guid>
<pubDate>Sun, 24 Aug 2025 10:52:32 +0000</pubDate>
</item>
<item>
<title>Primum nil nocere in PHP: First, Do No Harm</title>
<link>https://coderlegion.com/4912/primum-nil-nocere-in-php-first-do-no-harm</link>
<description>&lt;p&gt;Doctors have a saying: &lt;em&gt;primum nil nocere&lt;/em&gt; — “first, do no harm.” I first heard it in a medical context, but it stuck with me. Could the same principle apply to programming? Absolutely — especially when working with legacy code.&lt;/p&gt;
&lt;p&gt;If you work on legacy applications, you quickly learn one truth: every change can have unexpected side effects. The architecture is rigid, the design outdated, and the smallest tweak might break something miles away in the codebase. Still, we all want to make things better today than they were yesterday.&lt;/p&gt;
&lt;p&gt;That’s why I wrote a &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/distvan/legacy-php-hook&quot;&gt;pre-commit Git hook&lt;/a&gt; to apply the do no harm principle to my daily development. The idea is simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The whole codebase is under Git.&lt;/li&gt;
&lt;li&gt;Every commit should meet the current coding standard (PSR-12 in my case).&lt;/li&gt;
&lt;li&gt;Running &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/PHPCSStandards/PHP_CodeSniffer/&quot;&gt;PHP_CodeSniffer&lt;/a&gt; on the entire legacy project is slow and noisy.&lt;/li&gt;
&lt;li&gt;So instead, I only check the lines I’ve actually changed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If my changes violate the standard, the commit fails until I fix them. Over time, this improves the codebase without drowning me in thousands of old warnings.&lt;/p&gt;
&lt;p&gt;Sometimes, I can’t fix a file immediately because of other existing violations. In that case, the hook can just warn instead of blocking the commit, letting me decide what to do. You can also extend the script with your own rules — the setup is entirely yours.&lt;/p&gt;
&lt;p&gt;Each time you commit, you’re leaving the codebase a little cleaner, a little better. It’s like a doctor treating a patient: you’re not curing every illness at once, but you’re making sure you don’t make things worse.&lt;/p&gt;
&lt;p&gt;The script is in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/distvan/legacy-php-hook&quot;&gt;my repository&lt;/a&gt; — take it, use it, adapt it. And remember: even in PHP, first, do no harm.&lt;/p&gt;
</description>
<category>Best Practices</category>
<guid isPermaLink="true">https://coderlegion.com/4912/primum-nil-nocere-in-php-first-do-no-harm</guid>
<pubDate>Fri, 22 Aug 2025 07:17:09 +0000</pubDate>
</item>
<item>
<title>JuggleBee’s Great Leap - Rebuilding a Rails 4 App in Rails 8 (Part 1)</title>
<link>https://coderlegion.com/4894/jugglebees-great-leap-rebuilding-a-rails-4-app-in-rails-8-part-1</link>
<description>&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;http://www.jugglebee.com&quot;&gt;JuggleBee&lt;/a&gt; was born in 2015. It was Namibia's first online auction platform and is still one of the biggest today. I built it with Ruby on Rails 4.2 on Ruby 2.2. It sat there, rock-solid and stubbornly stable, only needing the occasional dust-off and minor tweak over the years. At various points in time, I thought about upgrading the various versions; however, I was stuck between a rock and a hard place — in order to upgrade Ruby, I had to upgrade Rails, and vice versa. The idea of incremental upgrades felt like a nightmare — two battlefronts to fight on, Ruby on one side and Rails on the other.&lt;/p&gt;
&lt;p&gt;Fast forward to 2025. With the arrival of my daughter, I acquired a smattering of free time interspersed with sleepless nights. The perfect time to finally bite the bullet, bring JuggleBee into the 21st century (you know what I mean), and get these upgrades done. After much pondering and planning, only one realistic option remained... incremental upgrades would take months with constant patching between every version bump; I would have to take the leap from Rails 4.2 straight to Rails 8, with Ruby hopping from 2.2 to 3.4.3. It was quite the journey, and what follows is a breakdown of the challenges and learnings I made along the way.&lt;/p&gt;
&lt;h3 id=&quot;starting-fresh&quot;&gt;Starting Fresh&lt;/h3&gt;
&lt;p&gt;Not to blow my own horn or anything, but I was in a pretty good place with this migration plan. The codebase of JuggleBee was intentionally built with self-contained business logic components, defining a clear separation of concerns. The models and controllers were not overly bloated, with liberal usage of &quot;service objects&quot; performing the more complicated logic, and an added separation of logic between my controllers and views using the &quot;view decorator&quot; pattern. This architecture made the copy-pasta approach almost… dare I say… fun. But let’s not kid ourselves — a jump from Rails 4.2 to 8 is no walk in the park.&lt;/p&gt;
&lt;p&gt;As part of the &lt;code&gt;rails new&lt;/code&gt; process (after installing the latest Ruby and necessary Rails gems), and before the true code migration could begin, I just needed an empty, runnable project with the necessary gems installed. I appended my old &lt;code&gt;Gemfile&lt;/code&gt; with the auto-generated one, removing all locked versions to ensure the latest compatible versions were installed. I also identified irrelevant and dead gems, removing those and replacing them with alternatives where necessary (more detail on this later).&lt;/p&gt;
&lt;p&gt;Reading into each of the default gems specified by the &lt;code&gt;rails new&lt;/code&gt; generator gave me a good understanding of what the current Rails stack looks like. I didn’t need all of it, so some were commented out, but I did take some of the new suggestions into account. &lt;code&gt;Puma&lt;/code&gt;, for example, replaced &lt;code&gt;Unicorn&lt;/code&gt;, which I was previously using. I could also get rid of &lt;code&gt;God&lt;/code&gt;, in the non-divine sense, as I no longer needed to monitor my server state manually. All in all, this whole process didn’t take more than an hour or so.&lt;/p&gt;
&lt;h3 id=&quot;models-controllers-services-amp-views-in-that-order-&quot;&gt;Models, Controllers, Services &amp;amp; Views (in that order)&lt;/h3&gt;
&lt;p&gt;These being purely Ruby code made it the easiest place to start. Even though we jumped up from Ruby 2.2 to 3.4.3, not all that much in the core language changed. Twas Rails that was the most behind. For example, JuggleBee was developed before the abstract &lt;code&gt;ApplicationRecord&lt;/code&gt; class made its way into the Rails architecture!&lt;/p&gt;
&lt;h4 id=&quot;models&quot;&gt;Models&lt;/h4&gt;
&lt;p&gt;The models were the easiest to migrate over, as they already had a smattering of unit tests behind them. First, though, we had to get the database up and running. Spoiler alert: I was also updating our Postgres version from &lt;strong&gt;9.6&lt;/strong&gt; to &lt;strong&gt;17.5&lt;/strong&gt; as part of this big update. The old migration files were fossils — brittle, outdated, and utterly unfit for a modern Rails 8 app. The simplest solution was to drop a &lt;code&gt;pg_dump&lt;/code&gt; of the current database (just on my local copy of the now legacy JuggleBee) and import it, generating the &lt;code&gt;schema.rb&lt;/code&gt;. I could now copy across my models and test them out in the &lt;code&gt;rails console&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Some of the updates required to get things up and running were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;belongs_to&lt;/code&gt; is now a required atribute. I have a home-grown version of polymorphism which allows a &lt;code&gt;Listing&lt;/code&gt; to be either a &lt;code&gt;Product&lt;/code&gt; or &lt;code&gt;Auction&lt;/code&gt; and the &lt;code&gt;belongs_to&lt;/code&gt; on these associations are optional, so I had to update these associations accordingly (*&lt;strong&gt;side note&lt;/strong&gt;: a refactor waiting to happen*). So,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;belongs_to :auction
belongs_to :product
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;becomes&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;belongs_to :auction, optional: true
belongs_to :product, optional: true
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I had a couple of instances of &lt;code&gt;update_attributes&lt;/code&gt; (very selective. Given they bypass validations, they were used with extreme caution) which is now deprecated and had to be changed to &lt;code&gt;update&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deprecated validation methods such as &lt;code&gt;validates_presence_of :name&lt;/code&gt; needed updating to &lt;code&gt;validates :name, presence: true&lt;/code&gt; sort of changes.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As for the views, the gems I used (&lt;code&gt;haml-rails&lt;/code&gt;, &lt;code&gt;redcarpet&lt;/code&gt;, &lt;code&gt;simple_form&lt;/code&gt;, &lt;code&gt;cocoon&lt;/code&gt;, etc) are all still actively maintained and required no changes at all — a very pleasant surprise.&lt;/p&gt;
&lt;h4 id=&quot;controllers&quot;&gt;Controllers&lt;/h4&gt;
&lt;p&gt;Next up were the controllers. Thanks to my use of service objects for the bulk of complex logic, migrating controllers was mostly painless. Rails 8 is stricter, and that’s where most of my changes were required. While bringing over controllers, I also began migrating the service objects they called upon, which are POROs and required almost no changes.&lt;/p&gt;
&lt;p&gt;The main updates included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Params passed to background jobs now must be valid JSON. This was especially relevant for controllers triggering email or background tasks.&lt;/li&gt;
&lt;li&gt;Controller &lt;code&gt;filter&lt;/code&gt;s were renamed to &lt;code&gt;action&lt;/code&gt;s (&lt;code&gt;before_filter&lt;/code&gt; → &lt;code&gt;before_action&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Controller hooks like &lt;code&gt;before_action&lt;/code&gt; should come after the method they reference, not at the top of the file (a style guideline enforced in modern Rails apps).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;redirect_to :back&lt;/code&gt; was replaced with &lt;code&gt;redirect_back fallback_location: root_path&lt;/code&gt; (or an appropriate fallback path).&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;responders&lt;/code&gt; gem is no longer included in Rails, so &lt;code&gt;respond_with&lt;/code&gt; calls were rewritten using &lt;code&gt;respond_to&lt;/code&gt; blocks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All in all, the MVC migration was more about cleaning things up and embracing modern Rails conventions rather than fighting major incompatibilities.&lt;/p&gt;
&lt;h3 id=&quot;javascript-modernization&quot;&gt;JavaScript Modernization&lt;/h3&gt;
&lt;p&gt;This is where things started to get spicy — the kind of spicy where you wonder if you just bit off more than you can chew. So much has changed in the front-end assets side of things. JuggleBee leverages &lt;strong&gt;jQuery&lt;/strong&gt; (thankfully I dodged the &lt;em&gt;CoffeeScript&lt;/em&gt; bullet) with Sprockets as the asset pipeline. Modern Rails favours &lt;code&gt;importmaps&lt;/code&gt; as the default asset manager and pipeline, and keeping with the spirit of rejuvenating JuggleBee and bringing it up to speed, I signed up to it too. This allowed me to replace a bunch of gems which added various front-end features, such as &lt;code&gt;chartkick&lt;/code&gt;, &lt;code&gt;masonry&lt;/code&gt; and &lt;code&gt;bootstrap&lt;/code&gt; with the importmaps &lt;code&gt;pin&lt;/code&gt; alternative. Some libraries had to remain as CDN imports in my layout file, due to the order at which these JavaScripts got initialized and loaded by the DOM.&lt;/p&gt;
&lt;p&gt;My suite of JavaScript classes also needed a bit of love and attention. The Sprockets&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//= require
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;statements were replaced with&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and my classes now had to &lt;code&gt;export&lt;/code&gt; themselves so that they could be loaded as ES modules. All the files had to be relocated from &lt;code&gt;app/assets/javascripts/&lt;/code&gt; to &lt;code&gt;app/javascript/&lt;/code&gt;. The way we add our JavaScript base file into our application view layout also had to change slightly, going from&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-erb&quot;&gt;&amp;lt;%= javascript_include_tag &quot;application&quot; %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-erb&quot;&gt;&amp;lt;%= javascript_importmap_tags %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even though &lt;code&gt;jQuery&lt;/code&gt; is considered a bit long in the tooth, given the plethora of really solid UI frameworks like Vue and React, I opted to keep my reliance on it for the time being. It still works, is actively maintained, and trying to replace it now would be a whole new kettle of fish. An adventure for another day.&lt;/p&gt;
&lt;h3 id=&quot;background-jobs-amp-scheduling&quot;&gt;Background Jobs &amp;amp; Scheduling&lt;/h3&gt;
&lt;p&gt;This area was one of the biggest wins in the entire migration. JuggleBee had the “classic” background processing setup: &lt;strong&gt;Sidekiq + Redis + whenever&lt;/strong&gt; (for cron). It worked fine for years, but it always felt a little… heavy, like lugging around a toolbox just to tighten a single screw. Sidekiq required its own container, Redis was a hungry beast that always felt like it was asking for more memory, and &lt;code&gt;whenever&lt;/code&gt; meant fiddling with cron jobs and ensuring everything stayed in sync across deploys. It worked, but it was yet another moving part in an already aging stack.&lt;/p&gt;
&lt;p&gt;Enter &lt;strong&gt;SolidQueue&lt;/strong&gt; — a gem that ships with Rails 8, built to replace Sidekiq-like setups with something that is both simpler and more tightly integrated with ActiveJob. The real beauty? &lt;strong&gt;No Redis.&lt;/strong&gt; SolidQueue uses the same database you already have (Postgres in our case) to manage job queues, which meant I could drop an entire service and reclaim a chunk of server memory.&lt;/p&gt;
&lt;p&gt;Here’s what this meant for JuggleBee:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No more Sidekiq configuration, container management, or Redis headaches.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;whenever&lt;/code&gt; gem (used for cron scheduling) could be thrown out too, thanks to SolidQueue’s built-in &lt;code&gt;recurring.yml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Background jobs now feel like a natural part of Rails — no extra glue required.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To put it in perspective, my stack went from:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Rails App + Sidekiq + Redis + whenever + cron
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;to&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Rails App + SolidQueue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s it. Beautiful, minimal, and a lot less to maintain.&lt;/p&gt;
&lt;p&gt;Another &lt;em&gt;chef’s kiss&lt;/em&gt; moment was how little code I had to change. Most jobs migrated over with minimal tweaks — the biggest adjustment being that job arguments &lt;strong&gt;must be JSON-serializable&lt;/strong&gt;. This meant replacing keyword arguments with hashes like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;EmailJob.perform(&quot;MyEmailClass&quot;, {&quot;from&quot; =&amp;gt; current_user.name, &quot;subject&quot; =&amp;gt; &quot;Hello there&quot;})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;instead of:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;EmailJob.perform(&quot;MyEmailClass&quot;, from: current_user.name, subject: &quot;Hello there&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It was one of those rare upgrades where less really is more. Dropping Redis and cron alone made this whole migration feel worth it.&lt;/p&gt;
&lt;h3 id=&quot;infrastructure-amp-deployment&quot;&gt;Infrastructure &amp;amp; Deployment&lt;/h3&gt;
&lt;p&gt;For years, deployment was powered by a custom shell script I wrote. It wasn’t flashy, but it was reliable and got the job done. It handled atomic releases, cleaned up old deployments, built Docker images, restarted containers, and even maintained a cache for faster updates. For a single-developer project like JuggleBee, it was the perfect balance of simplicity and control.&lt;/p&gt;
&lt;p&gt;Here’s the core of that script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash

branch=${1:-master}
cwd=/home/ubuntu
repo_url=your_repo_here
revision=master
keep=5
timestamp=$(date +%s)
cache=$cwd/.git_cache
releases=$cwd/releases
mkdir -p $releases
previous=$releases/$(ls $releases/ -t | head -n 1)
release=$releases/$timestamp

if [ ! -d $cache ] ; then
        git clone $repo_url $cache
fi

echo &quot;downloading latest source code from $branch...&quot;
mkdir -p $release
(cd $cache &amp;amp;&amp;amp; git gc --prune=now &amp;amp;&amp;amp; git remote prune origin &amp;amp;&amp;amp; git fetch origin &amp;amp;&amp;amp; git reset --hard origin/$branch &amp;amp;&amp;amp; git archive $revision | tar -x -c $release)

echo &quot;release timestamp is: $timestamp ($release)&quot;

echo 'building docker images...'
/bin/bash -c &quot;(cd $release &amp;amp;&amp;amp; docker compose -f docker-production.yml build)&quot;

if [ $? -eq 0 ]
then
        if [[ $previous != &quot;&quot; ]] ; then
                echo 'stopping previous containers...'
                /bin/bash -c &quot;(cd $previous &amp;amp;&amp;amp; docker compose -f docker-production.yml stop &amp;amp;&amp;amp; docker ps -a -q --filter='status=exited' | xargs docker rm)&quot;
        fi

        echo 'restarting application...'
        /bin/bash -c &quot;(cd $release &amp;amp;&amp;amp; docker compose -f docker-production.yml up -d)&quot;

        echo 'deleting old releases...'
        (cd $releases &amp;amp;&amp;amp; (ls -t | head -n $keep; ls) | sort | uniq -u | xargs rm -rf)

        echo 'creating symlink.'
        rm $cwd/current-release
        ln -s $release $cwd/current-release

        echo 'deploy complete.'
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Honestly, this script could have kept soldiering on for years… until &lt;strong&gt;Kamal&lt;/strong&gt; walked in like the cool new kid who makes your old tricks look ancient. It wasn’t broken — far from it — but Kamal is like having a personal DevOps engineer in your terminal. It takes all the things I cared about (Dockerized deploys, atomic releases, rollbacks) and adds extra superpowers I didn’t even realize I needed.&lt;/p&gt;
&lt;h3 id=&quot;introducing-kamal-2&quot;&gt;Introducing Kamal 2&lt;/h3&gt;
&lt;p&gt;Kamal is a server provisioning and deployment tool built specifically (but not limited to) Rails apps. It automates a ton of tasks that my script didn’t — and one of the most significant changes? &lt;strong&gt;No more Nginx.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Kamal ships with a built-in Traefik proxy that handles all your routing and SSL certificates out of the box. No more tinkering with &lt;code&gt;nginx.conf&lt;/code&gt; files or restarting a separate webserver. One less dependency chewing through RAM and one less thing to break at 2 am.&lt;/p&gt;
&lt;p&gt;Here’s what Kamal brings to the table:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;kamal setup&lt;/code&gt;&lt;/strong&gt;: installs Docker, Git, and all required dependencies on your server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;kamal deploy&lt;/code&gt;&lt;/strong&gt;: builds your Rails app into a Docker image, pushes it to your registry, and runs it on the server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Traefik proxy with SSL (via Let’s Encrypt)&lt;/strong&gt;: you get HTTPS without lifting a finger.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accessory management&lt;/strong&gt;: want Postgres or Redis? Kamal spins them up as Docker containers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scaling&lt;/strong&gt;: need more capacity? add another container in deploy.yml and Kamal will route traffic automatically.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The impact of this was huge. I went from managing Docker builds, old release directories, and an Nginx webserver, to just running one command. There’s something almost unsettling about how smooth it is — I felt like I was missing steps at first, like forgetting my keys when leaving the house.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Wait, that’s it? Did I actually deploy? Did I forget something? Oh… it’s done already. Huh.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This wasn’t just a convenience win — it was a dependency detox. Between dropping Nginx here and killing Redis with SolidQueue earlier, my infrastructure got a lot lighter, faster, and cheaper to run. Kamal didn’t just evolve my old script — it rendered half of its logic unnecessary.&lt;/p&gt;
&lt;h3 id=&quot;half-way-there&quot;&gt;Half Way There&lt;/h3&gt;
&lt;p&gt;With the new Rails 8 foundation in place, JuggleBee has already shed a lot of its old skin. We’ve modernized the models and controllers, swapped out a creaky asset pipeline for importmaps, retired Sidekiq, Redis, and cron in favor of SolidQueue, and traded a homegrown deployment script (solid as it was) for the convenience and power of Kamal 2. The app is lighter, faster to deploy, and much easier to maintain — but we’re not done yet.&lt;/p&gt;
&lt;p&gt;Getting the Rails 8 app running and deployable was only &lt;strong&gt;half the battle&lt;/strong&gt;. Now comes the rest of the journey:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Migrating data from a legacy PostgreSQL 9.6 database to a fresh PostgreSQL 17.5 instance.&lt;/li&gt;
&lt;li&gt;Replacing CarrierWave with ActiveStorage and migrating all user-uploaded files into a new S3 bucket.&lt;/li&gt;
&lt;li&gt;Locking down credentials with Rails encrypted secrets and Bitwarden CLI.&lt;/li&gt;
&lt;li&gt;Cleaning up leftover code smells, finalizing test coverage, and polishing the overall performance of the new stack.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s some of what Part 2 will cover — all the finishing touches, the unexpected hurdles, and the final steps that took JuggleBee from a Rails 4 relic to a lean, modern, and production-ready Rails 8 app.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stay tuned for Part 2, where the migration story continues.&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Ruby on Rails</category>
<guid isPermaLink="true">https://coderlegion.com/4894/jugglebees-great-leap-rebuilding-a-rails-4-app-in-rails-8-part-1</guid>
<pubDate>Thu, 21 Aug 2025 08:06:05 +0000</pubDate>
</item>
<item>
<title>While everyone’s chasing AI and Data Science… COBOL is quietly running the world.</title>
<link>https://coderlegion.com/4491/while-everyones-chasing-ai-and-data-science-cobol-is-quietly-running-the-world</link>
<description>&lt;p&gt;You’ve heard the stat: COBOL powers 95% of ATM transactions — but that’s just scratching the surface.&lt;/p&gt;
&lt;p&gt; The real story? COBOL is everywhere:&lt;/p&gt;
&lt;p&gt;→ 85% of insurance claims are processed using COBOL&lt;br&gt;
→ Major airline reservation systems still run on it&lt;br&gt;
→ U.S. military missile defense? COBOL.&lt;br&gt;
→ Social Security handles 250M+ records daily — with COBOL&lt;/p&gt;
&lt;p&gt;And yet, it’s not talked about — because it just works.&lt;/p&gt;
&lt;p&gt; Here’s the surprising truth:&lt;/p&gt;
&lt;p&gt;COBOL isn’t outdated. It’s battle-tested, high-performance, and ultra-reliable:&lt;/p&gt;
&lt;p&gt;• 220+ billion lines of active code&lt;br&gt;
• $3 trillion processed daily&lt;br&gt;
• Systems with 30+ years of uptime&lt;br&gt;
• Powers 95% of global ATM swipes&lt;/p&gt;
&lt;p&gt;Failed modernizations prove the point:&lt;/p&gt;
&lt;p&gt;– One bank lost over $100M trying to replace COBOL&lt;br&gt;
– Another gave up after 7 years of migration hell&lt;br&gt;
– An insurer lost decades of customer data&lt;/p&gt;
&lt;p&gt;So, why does COBOL persist?&lt;/p&gt;
&lt;p&gt;99.999% uptime – almost never fails&lt;/p&gt;
&lt;p&gt;2M+ transactions/sec – unmatched speed&lt;/p&gt;
&lt;p&gt;50+ years of security hardening&lt;/p&gt;
&lt;p&gt;Mathematical precision – critical for finance&lt;/p&gt;
&lt;p&gt; While the tech world obsesses over the next big thing, COBOL remains the unshakable foundation of global finance.&lt;/p&gt;
&lt;p&gt;Sometimes the most important technology...&lt;br&gt;
is the one that’s already doing the job better than anything else.&lt;/p&gt;
</description>
<category>Java</category>
<guid isPermaLink="true">https://coderlegion.com/4491/while-everyones-chasing-ai-and-data-science-cobol-is-quietly-running-the-world</guid>
<pubDate>Thu, 07 Aug 2025 14:04:08 +0000</pubDate>
</item>
</channel>
</rss>