<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged migration</title>
<link>https://coderlegion.com/tag/migration</link>
<description>Powered by Question2Answer</description>
<item>
<title>The Pitfalls of Migrating from Xamarin.Forms to .NET MAUI</title>
<link>https://coderlegion.com/29655/the-pitfalls-of-migrating-from-xamarin-forms-to-net-maui</link>
<description>&lt;h3 id=&quot;why-the-migration-is-not-a-simple-upgrade&quot;&gt;Why the migration is not a simple upgrade&lt;/h3&gt;
&lt;p&gt;On paper, migrating from Xamarin.Forms to .NET MAUI looks like a namespace change. In practice, the time goes into three areas: custom renderers, navigation, and plugins that are no longer maintained. Microsoft's support for Xamarin ended in May 2024, and the apps still on it are getting harder to publish as Apple and Google raise their minimum SDK requirements.&lt;br&gt;
The mechanical part is relatively easy. The project moves to the single-project format, &lt;code&gt;Xamarin.Forms&lt;/code&gt; becomes &lt;code&gt;Microsoft.Maui.Controls&lt;/code&gt;, and &lt;code&gt;Xamarin.Essentials&lt;/code&gt; is built directly into the framework. Automated upgrade tools and AI assistants handle this part well.&lt;br&gt;
What they don't handle well is understanding why a custom renderer existed, what the code assumed about the order in which pages load, or what hidden behavior an old plugin provided. This article walks through those pitfalls, with code examples and solutions that work in real projects.&lt;/p&gt;
&lt;h3 id=&quot;from-renderers-to-handlers&quot;&gt;From renderers to handlers&lt;/h3&gt;
&lt;p&gt;The biggest conceptual change is that MAUI no longer customizes controls through inheritance, but through mappers: dictionaries that link each property of a cross-platform control to the native code that applies it.&lt;br&gt;
In Xamarin.Forms, removing the underline from an &lt;code&gt;Entry&lt;/code&gt; on Android meant writing an entire renderer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[assembly: ExportRenderer(typeof(BorderlessEntry), typeof(BorderlessEntryRenderer))]
public class BorderlessEntryRenderer : EntryRenderer
{
    public BorderlessEntryRenderer(Context context) : base(context) { }

    protected override void OnElementChanged(ElementChangedEventArgs&amp;lt;Entry&amp;gt; e)
    {
        base.OnElementChanged(e);
        if (Control != null)
            Control.Background = null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In MAUI, the same effect comes from adding an entry to the existing mapper, usually in &lt;code&gt;MauiProgram.cs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping(&quot;Borderless&quot;, (handler, view) =&amp;gt;
{
    if (view is not BorderlessEntry)
        return;
#if ANDROID
    handler.PlatformView.Background = null;
#elif IOS || MACCATALYST
    handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;
#endif
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;pitfall-1-mappers-are-global&quot;&gt;Pitfall 1: mappers are global&lt;/h2&gt;
&lt;p&gt;A renderer applied only to the type registered with &lt;code&gt;ExportRenderer&lt;/code&gt;. An entry added to &lt;code&gt;EntryHandler.Mapper&lt;/code&gt; applies to every &lt;code&gt;Entry&lt;/code&gt; in the app. The &lt;code&gt;if (view is not BorderlessEntry) return;&lt;/code&gt; check in the example above is not optional. Without it, every form in the app loses its border, and the bug only shows up during testing, on screens you weren't looking at.&lt;/p&gt;
&lt;h2 id=&quot;pitfall-2-onelementchanged-has-no-direct-equivalent&quot;&gt;Pitfall 2: OnElementChanged has no direct equivalent&lt;/h2&gt;
&lt;p&gt;Old renderers mixed three things in &lt;code&gt;OnElementChanged&lt;/code&gt;: creating the native control, subscribing to events, and unsubscribing. In MAUI these are separate. For a custom handler, the native control is created in &lt;code&gt;CreatePlatformView&lt;/code&gt;, subscriptions go in &lt;code&gt;ConnectHandler&lt;/code&gt;, and cleanup goes in &lt;code&gt;DisconnectHandler&lt;/code&gt;. Moving the old code without splitting it across these three methods usually leads to memory leaks or events that fire twice.&lt;br&gt;
Also watch when &lt;code&gt;DisconnectHandler&lt;/code&gt; gets called. In early MAUI versions it had to be called manually, while newer versions have a configurable policy via &lt;code&gt;HandlerProperties.DisconnectPolicy&lt;/code&gt;. Check the documentation for the .NET version you're using.&lt;/p&gt;
&lt;h2 id=&quot;pitfall-3-the-temptation-of-compatibility-renderers&quot;&gt;Pitfall 3: the temptation of compatibility renderers&lt;/h2&gt;
&lt;p&gt;MAUI lets you register old renderers through &lt;code&gt;AddCompatibilityRenderer&lt;/code&gt;. That's useful for getting the app running quickly, but it's technical debt: the compatibility package is marked obsolete and doesn't benefit from the new pipeline's optimizations. My recommendation is to use it only as an intermediate step, with a clear ticket for each remaining renderer.&lt;/p&gt;
&lt;h2 id=&quot;pitfall-4-the-native-control-type-has-changed&quot;&gt;Pitfall 4: the native control type has changed&lt;/h2&gt;
&lt;p&gt;Old code often cast to specific native classes. In MAUI, &lt;code&gt;handler.PlatformView&lt;/code&gt; may be a MAUI-derived class (for example &lt;code&gt;MauiTextField&lt;/code&gt; on iOS), not the UIKit or Android class you knew. A wrong cast doesn't produce a compile error but an &lt;code&gt;InvalidCastException&lt;/code&gt; at runtime, usually on only one platform.&lt;/p&gt;
&lt;h3 id=&quot;shell-and-navigation&quot;&gt;Shell and navigation&lt;/h3&gt;
&lt;p&gt;Shell existed in Xamarin.Forms too, but many older apps used &lt;code&gt;NavigationPage&lt;/code&gt;, &lt;code&gt;TabbedPage&lt;/code&gt;, and &lt;code&gt;MasterDetailPage&lt;/code&gt;. In MAUI, Shell becomes the recommended approach, and moving to it changes more than the syntax.&lt;/p&gt;
&lt;h2 id=&quot;pitfall-5-shell-doesn-t-accept-every-page-type&quot;&gt;Pitfall 5: Shell doesn't accept every page type&lt;/h2&gt;
&lt;p&gt;A &lt;code&gt;ShellContent&lt;/code&gt; must contain a &lt;code&gt;ContentPage&lt;/code&gt;. A &lt;code&gt;TabbedPage&lt;/code&gt; or &lt;code&gt;FlyoutPage&lt;/code&gt; (formerly &lt;code&gt;MasterDetailPage&lt;/code&gt;) placed inside Shell is not supported. Tabs and the side menu are defined through the Shell structure (&lt;code&gt;TabBar&lt;/code&gt;, &lt;code&gt;Tab&lt;/code&gt;, &lt;code&gt;FlyoutItem&lt;/code&gt;). If the app had tabs nested inside detail pages, the structure has to be rethought, not just moved.&lt;/p&gt;
&lt;h2 id=&quot;pitfall-6-routes-and-parameters&quot;&gt;Pitfall 6: routes and parameters&lt;/h2&gt;
&lt;p&gt;Pages that don't appear in the Shell visual hierarchy must be registered explicitly, otherwise navigation fails at runtime:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Routing.RegisterRoute(nameof(PatientDetailPage), typeof(PatientDetailPage));

await Shell.Current.GoToAsync($&quot;{nameof(PatientDetailPage)}?id={patient.Id}&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two details take the most time. First, the &lt;code&gt;//&lt;/code&gt; prefix means an absolute route and resets the navigation stack, which is right after login but wrong in a list-detail flow. Second, query string parameters are text. For complex objects, use the parameter dictionary (or &lt;code&gt;ShellNavigationQueryParameters&lt;/code&gt; in recent versions, valid for a single navigation) and receive them through &lt;code&gt;IQueryAttributable&lt;/code&gt;, not through manual serialization in the URL.&lt;/p&gt;
&lt;h2 id=&quot;pitfall-7-page-lifecycle&quot;&gt;Pitfall 7: page lifecycle&lt;/h2&gt;
&lt;p&gt;With &lt;code&gt;ContentTemplate&lt;/code&gt;, Shell creates pages the first time they are shown, not when the app starts. Code that assumed a ViewModel already existed (for example, subscribing in a constructor to messages sent by another page) can miss events. In addition, pages in tabs usually stay in memory, so &lt;code&gt;OnAppearing&lt;/code&gt; is called every time you return, not just once. Data-loading logic needs to be idempotent.&lt;/p&gt;
&lt;h2 id=&quot;pitfall-8-dependency-injection-in-pages&quot;&gt;Pitfall 8: dependency injection in pages&lt;/h2&gt;
&lt;p&gt;MAUI has built-in DI, and Shell can resolve pages and ViewModels from the container if they are registered in &lt;code&gt;MauiProgram.cs&lt;/code&gt;. Pay attention to the lifetime you choose: a page registered as a singleton keeps its state between navigations, while a transient one loses it. Code that used &lt;code&gt;DependencyService.Get&amp;lt;T&amp;gt;()&lt;/code&gt; still works, but this is a good moment to move it to constructor injection.&lt;/p&gt;
&lt;h3 id=&quot;plugins-that-no-longer-exist&quot;&gt;Plugins that no longer exist&lt;/h3&gt;
&lt;p&gt;In many projects, plugins block the migration more than your own code does. The Xamarin ecosystem was built on community-maintained packages: some were abandoned, some were folded into the framework, and others were rewritten under a different name. The first step is an inventory of every NuGet package, with a replacement for each one.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Xamarin.Essentials - built into MAUI - namespaces change (&lt;code&gt;Microsoft.Maui.Devices&lt;/code&gt;, &lt;code&gt;Microsoft.Maui.Storage&lt;/code&gt;, &lt;code&gt;Microsoft.Maui.ApplicationModel&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Plugin.Permissions - &lt;code&gt;Permissions&lt;/code&gt; (built in) - similar API, but manifest/Info.plist declarations are still required&lt;/li&gt;
&lt;li&gt;Plugin.Connectivity - &lt;code&gt;Connectivity&lt;/code&gt; (built in)&lt;/li&gt;
&lt;li&gt;Plugin.Media - MediaPicker (built in) - for full camera control, CommunityToolkit.Maui.Camera&lt;/li&gt;
&lt;li&gt;Xamarin.CommunityToolkit - CommunityToolkit.Maui - many APIs renamed; check behaviors and converters one by one&lt;/li&gt;
&lt;li&gt;Rg.Plugins.Popup - Mopups or Popup from CommunityToolkit.Maui - Mopups keeps a similar API, faster migration&lt;/li&gt;
&lt;li&gt;FFImageLoading - standard &lt;code&gt;Image&lt;/code&gt; or community forks for MAUI - the original package is no longer maintained&lt;/li&gt;
&lt;li&gt;Xamarin.Forms.Maps - Microsoft.Maui.Controls.Maps - requires &lt;code&gt;UseMauiMaps()&lt;/code&gt; in &lt;code&gt;MauiProgram.cs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;SkiaSharp.Views.Forms - SkiaSharp.Views.Maui.Controls - requires UseSkiaSharp()&lt;/li&gt;
&lt;li&gt;Lottie (Xamarin) - &lt;code&gt;SKLottieView&lt;/code&gt; from SkiaSharp.Extended.UI.Maui&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MessagingCenter&lt;/code&gt; - &lt;code&gt;WeakReferenceMessenger&lt;/code&gt; from CommunityToolkit.Mvvm - &lt;code&gt;MessagingCenter&lt;/code&gt; is marked obsolete&lt;/li&gt;
&lt;li&gt;Visual Studio App Center - Sentry, Firebase Crashlytics, etc. - App Center was retired in March 2025&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;pitfall-9-the-plugin-works-but-only-on-one-platform&quot;&gt;Pitfall 9: the plugin “works”, but only on one platform&lt;/h2&gt;
&lt;p&gt;Some packages have versions that install without errors in a MAUI project but cover only some platforms or depend on old native libraries. The build passes, and the problem appears in the iOS build, at publishing time, or on the first run on a real device. Check on NuGet which frameworks each package targets (for example &lt;code&gt;net8.0-android&lt;/code&gt;, &lt;code&gt;net8.0-ios&lt;/code&gt;) and the date of its latest release.&lt;/p&gt;
&lt;h2 id=&quot;pitfall-10-hidden-behaviors&quot;&gt;Pitfall 10: hidden behaviors&lt;/h2&gt;
&lt;p&gt;Old plugins often did things you couldn't see in your code. Plugin.Media, for example, requested permissions on its own and could compress or rotate the image. &lt;code&gt;MediaPicker&lt;/code&gt; doesn't do all of that, so after migration you may get photos several MB in size or with the wrong orientation. Every replacement has to be tested for behavior, not just for compilation.&lt;/p&gt;
&lt;h3 id=&quot;small-pitfalls-that-eat-up-time&quot;&gt;Small pitfalls that eat up time&lt;/h3&gt;
&lt;p&gt;None of the issues below is hard, but together they can add whole days to an estimate.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Image resources&lt;/strong&gt;. In the single project, images live in &lt;code&gt;Resources/Images&lt;/code&gt; and must follow Android rules: lowercase letters, digits and underscores, no hyphens. SVG files are converted to PNG at build time and are referenced in XAML with the &lt;code&gt;.png&lt;/code&gt; extension.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layouts&lt;/strong&gt;. Default spacing values have changed, &lt;code&gt;RelativeLayout&lt;/code&gt; has moved to the compatibility package, and &lt;code&gt;Frame&lt;/code&gt; is replaced by &lt;code&gt;Border&lt;/code&gt;. Screens look “almost” the same, which is more dangerous than looking obviously broken.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Device APIs&lt;/strong&gt;. &lt;code&gt;Device.RuntimePlatform&lt;/code&gt; becomes &lt;code&gt;DeviceInfo.Platform&lt;/code&gt;, and &lt;code&gt;Device.BeginInvokeOnMainThread&lt;/code&gt; becomes &lt;code&gt;MainThread.BeginInvokeOnMainThread&lt;/code&gt; or &lt;code&gt;Dispatcher&lt;/code&gt;. The old code compiles with warnings, so it's easy to ignore.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ListView vs. CollectionView&lt;/strong&gt;. &lt;code&gt;ListView&lt;/code&gt; still exists, but &lt;code&gt;CollectionView&lt;/code&gt; is the recommended option and behaves differently for selection, separators and row height.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compiled bindings&lt;/strong&gt;. Adding x:DataType in XAML brings a visible performance gain and catches, at compile time, wrong bindings that failed silently in Xamarin.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trimming and AOT&lt;/strong&gt;. Release builds can strip code accessed through reflection (serialization, dynamically configured DI). An app that works perfectly in Debug can stop at startup in Release. Test the Release build on a real device in the first week, not at the end.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;conclusion-a-lower-risk-strategy&quot;&gt;Conclusion: a lower-risk strategy&lt;/h3&gt;
&lt;p&gt;A successful migration starts with the inventory, not the code. The order that has worked best in practice is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Inventory every NuGet package, custom renderer, effect and DependencyService call, with a replacement decided for each.&lt;/li&gt;
&lt;li&gt;Do the mechanical part (single project, namespaces, Essentials) and get the app compiling, temporarily using compatibility renderers where needed.&lt;/li&gt;
&lt;li&gt;Build and run the Release build on real devices, on both platforms, before any other change.&lt;/li&gt;
&lt;li&gt;Rewrite renderers as handlers or mappers, one at a time, with visual tests on every affected screen.&lt;/li&gt;
&lt;li&gt;Move navigation to Shell only after everything else is stable. It's the change with the biggest impact on behavior.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;MAUI is a cleaner and faster framework than Xamarin.Forms, and the handler model makes customization much simpler once you understand it. The real cost of the migration lies in what the old code did implicitly. If you identify those things early, the estimate stays realistic and the app reaches the stores without surprises.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29655/the-pitfalls-of-migrating-from-xamarin-forms-to-net-maui</guid>
<pubDate>Sat, 03 Oct 2026 17:04:00 +0000</pubDate>
</item>
<item>
<title>What Is WordPress Migration and When Should You Consider It?</title>
<link>https://coderlegion.com/25352/what-is-wordpress-migration-and-when-should-you-consider-it</link>
<description>&lt;p&gt;Your website has been live for a while now. Traffic is slowly climbing, your content library is growing, and things seem to be moving in the right direction. But lately, pages are taking forever to load, your hosting plan feels suffocating, or maybe you're just tired of fighting with a clunky CMS that doesn't let you do what you want.&lt;/p&gt;
&lt;p&gt;Sound familiar? You're not alone. And more often than not, the answer to these frustrations comes down to one decision — moving your site to a better, more capable setup.&lt;br&gt;
That's exactly where WordPress migration comes in.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;So, What Exactly Is WordPress Migration?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;At its core, &lt;a rel=&quot;nofollow&quot; href=&quot;https://wpwebinfotech.com/wordpress-development/migration/&quot;&gt;WordPress migration&lt;/a&gt; refers to the process of moving your website from one location to another. That could mean switching hosting providers, moving from a subdomain to a live domain, transferring from a local development environment to a production server, or even switching from a completely different CMS to WordPress altogether.&lt;/p&gt;
&lt;p&gt;It sounds straightforward, but there's more to it than just copying files from one place to another. A proper migration involves moving your WordPress installation, your database, your media files, your themes, and all your plugins — without breaking anything in the process.&lt;/p&gt;
&lt;p&gt;When done right, your visitors won't notice a thing. The site keeps running, everything loads correctly, and no data gets left behind. When done wrong, you could be looking at broken links, missing images, or dreaded downtime.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Different Types of WordPress Migration&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Not every migration looks the same. Depending on what you're trying to accomplish, the process can vary quite a bit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Host-to-Host Migration&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is the most common type. You're keeping your WordPress site as-is but moving it to a new hosting provider. Maybe your current host is slow, expensive, or just not cutting it anymore. Moving to a faster, more reliable server can make a significant difference in both performance and peace of mind.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Local to Live Migration&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Developers often build websites in a local environment first before pushing them live. Once everything is tested and ready, the site needs to be migrated to a live server. This type of migration requires careful setup to make sure nothing breaks when the site goes public.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CMS to WordPress Migration&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is where things get a little more involved. If you're currently running your site on Wix, Squarespace, Joomla, or another platform, and you've decided to move to WordPress, that's a full platform migration. Your content, structure, and sometimes even your design will need to be rebuilt or carefully transferred over.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Domain or Subdomain Migration&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Sometimes the move isn't about the host at all. Maybe you're rebranding, changing your domain name, or moving a site from a subdomain like staging.yoursite.com to your main domain. This involves DNS updates and redirect strategies to keep your SEO intact.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why Do People Migrate to WordPress in the First Place?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;WordPress powers over 40% of the web for a reason. It offers a level of flexibility, control, and scalability that most other platforms simply can't match. Whether you're running a blog, an ecommerce store, or a full-scale business site, WordPress gives you the tools to grow without constantly hitting walls.&lt;/p&gt;
&lt;p&gt;If you're coming from another platform, you've probably already felt those walls. Limited customization. Clunky interfaces. Plugins that cost a fortune and still don't do what you need.&lt;br&gt;
Migrating to a self-hosted WordPress setup removes those barriers. You get full control over your site's design, functionality, and data. You're not locked into anyone else's ecosystem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When Should You Actually Consider Migrating?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is the big question. Migrating a website isn't something you do on a whim — it takes planning, a solid backup, and some technical patience. But there are clear signs that tell you it's time to make the move.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Your Site Is Getting Slower&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Page speed isn't just a user experience issue — it directly impacts your search rankings. If your current hosting plan is struggling to keep up with your traffic, you'll notice it in your loading times. A sluggish site frustrates visitors and sends them elsewhere. Moving to a better host or optimizing your setup can turn this around quickly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You've Outgrown Your Current Plan&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Hosting plans come with limits — storage, bandwidth, the number of sites you can run. If you're bumping up against those limits regularly, it's a signal that your site has grown beyond what your current setup can comfortably handle. Upgrading within your current host is one option, but sometimes a full migration to a new provider makes more financial and technical sense.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Your Current CMS Doesn't Give You Enough Control&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Maybe you're on a platform that looked great when you were just starting out, but now it's holding you back. You can't install the plugins you need. Customizing your design requires a workaround. The ecommerce features are limited or expensive. WordPress opens up an enormous ecosystem of plugins, themes, and integrations that can support almost any direction you want to take your site.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security and Reliability Are a Concern&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If your site has been hacked, experienced significant downtime, or your host has been slow to respond to support requests, that's a problem you shouldn't keep tolerating. Reliable WordPress hosting providers take security seriously — they run regular updates, offer automated backups, and provide proper support when things go sideways.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You're Redesigning or Rebranding&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A site redesign is often the perfect time to reconsider your entire setup. If you're rebuilding your site anyway, migrating to a new host or consolidating your domain structure at the same time just makes sense. It's much easier to handle these changes together than to do them separately later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What Are the Risks You Should Know About?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It's worth being upfront here. WordPress migration, when handled carelessly, can cause real problems. These include broken internal links, missing media files, plugin conflicts, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.conbersa.ai/learn/what-is-seo-disruption&quot;&gt;SEO disruptions&lt;/a&gt;, and temporary downtime if the DNS transition isn't handled properly.&lt;/p&gt;
&lt;p&gt;That's why preparation matters so much. Before any migration, you should always create a complete backup of your site. Test the new environment thoroughly before making it live. And put proper redirects in place to protect your search rankings.&lt;/p&gt;
&lt;p&gt;If this feels overwhelming, that's okay. Many hosting providers offer free migration assistance, and there are experienced WordPress developers and agencies who handle migrations regularly. Getting professional help for a complex migration is always a smarter call than rushing through it yourself and dealing with the aftermath.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;WordPress migration isn't just a technical task — it's a strategic decision that can genuinely change the trajectory of your website. Whether you're switching hosts, moving platforms, or finally taking your site from development to live, a successful migration sets you up for better performance, more control, and long-term growth. And if the process feels too complex to handle solo, partnering with experienced &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.cloudways.com/blog/wordpress-agencies/&quot;&gt;WordPress agencies&lt;/a&gt; can make the whole thing significantly smoother — they've done it hundreds of times and know exactly where things tend to go wrong.&lt;/p&gt;
&lt;p&gt;The key is knowing when it's the right time to make the move and going into it with a clear plan. If your site is slow, limited, or simply not working the way it should, don't keep waiting for things to improve on their own. Migration might be exactly the reset your website needs — and when done right, it's one of the best investments you can make in your online presence.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25352/what-is-wordpress-migration-and-when-should-you-consider-it</guid>
<pubDate>Tue, 25 Aug 2026 13:25:13 +0000</pubDate>
</item>
<item>
<title>Squarespace → WordPress Migration: A Developer's Cost &amp; Scope Checklist</title>
<link>https://coderlegion.com/25148/squarespace-wordpress-migration-a-developers-cost-scope-checklist</link>
<description>&lt;p&gt;If you're a freelance dev quoting a Squarespace-to-WordPress migration, pricing it like a standard &quot;content move&quot; undersells the work — and pricing it like a full custom build oversells it to the client. Here's how to scope it properly.&lt;/p&gt;
&lt;p&gt;What's actually involved (not just content transfer):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Squarespace templates are proprietary and don't export — you're rebuilding the design in WordPress, not migrating it&lt;/li&gt;
&lt;li&gt;Full URL audit + 301 redirect map (this is the technical SEO-critical piece most junior devs underscope)&lt;/li&gt;
&lt;li&gt;Metadata transfer (titles, meta descriptions, alt text) — often manually re-entered since export is inconsistent&lt;/li&gt;
&lt;li&gt;Reconnecting third-party integrations (forms, booking tools, email capture)&lt;/li&gt;
&lt;li&gt;Staging environment testing before DNS cutover&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Where devs typically underquote:&lt;br&gt;
E-commerce. Moving Squarespace Commerce to WooCommerce isn't &quot;importing products&quot; — it's rebuilding the catalog, payment gateway setup, and checkout flow from scratch. Price this as a separate line item, not a content-migration add-on.&lt;/p&gt;
&lt;p&gt;Rough market rates for reference: $400–$1,200 for a standard business site (10–30 pages), $800–$2,500+ once e-commerce or heavy custom design enters the scope.&lt;/p&gt;
&lt;p&gt;Full breakdown with a step-by-step process and a real project example: &lt;a rel=&quot;nofollow&quot; href=&quot;https://amanurrahman.com/blog-post/squarespace-to-wordpress-migration-cost&quot;&gt;https://amanurrahman.com/blog-post/squarespace-to-wordpress-migration-cost&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25148/squarespace-wordpress-migration-a-developers-cost-scope-checklist</guid>
<pubDate>Sat, 22 Aug 2026 16:14:17 +0000</pubDate>
</item>
<item>
<title>Wix to WordPress: What the Migration Actually Costs</title>
<link>https://coderlegion.com/24324/wix-to-wordpress-what-the-migration-actually-costs</link>
<description>&lt;p&gt;Developers get asked this constantly: &quot;I want to move off Wix — what's it going to cost me?&quot; The frustrating truth is there's no flat answer, because Wix simply doesn't offer a clean export. Every migration is partly a manual rebuild.&lt;/p&gt;
&lt;p&gt;Rough numbers based on real small-business projects: a basic 5-15 page site with standard sections runs $300-$900. Add custom design elements or grow past 15-20 pages and you're looking at $900-$2,200. If there's a Wix Stores catalog involved, migrating to WooCommerce (products, shipping rules, payment gateway) pushes it to $1,500-$4,000+.&lt;/p&gt;
&lt;p&gt;The part that gets skipped most often, and costs the most to fix later, is SEO continuity. Wix uses absolute-positioned layouts; WordPress themes are responsive. That's a real rebuild, not a copy-paste. And if you don't map every old URL to a 301 redirect before launch, Google treats the new site as entirely new — every backlink and ranking signal from the Wix era just evaporates.&lt;/p&gt;
&lt;p&gt;Full pricing breakdown and the redirect/SEO checklist: &lt;a rel=&quot;nofollow&quot; href=&quot;https://amanurrahman.com/blog-post/wix-to-wordpress-migration-cost&quot;&gt;https://amanurrahman.com/blog-post/wix-to-wordpress-migration-cost&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24324/wix-to-wordpress-what-the-migration-actually-costs</guid>
<pubDate>Sun, 09 Aug 2026 07:02:48 +0000</pubDate>
</item>
<item>
<title>WordPress Migration: What It Actually Costs (and How Not to Lose Rankings)</title>
<link>https://coderlegion.com/22970/wordpress-migration-what-it-actually-costs-and-how-not-to-lose-rankings</link>
<description>&lt;p&gt;&quot;Migration&quot; gets used for a few different jobs, so here's how I price each:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hosting migration (same site, new host): $150+&lt;/li&gt;
&lt;li&gt;Static HTML to WordPress: $500+&lt;/li&gt;
&lt;li&gt;Platform migration (Shopify/Wix/Squarespace → WordPress): $700+&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The technical detail that matters most: URL redirect mapping. Skip it and you lose rankings — not because of the platform change, but because of broken links Google can no longer find.&lt;/p&gt;
&lt;p&gt;Full writeup with pricing table and a 5-step SEO-safe migration checklist: &lt;a rel=&quot;nofollow&quot; href=&quot;https://amanurrahman.com/blog-post/wordpress-website-migration-cost&quot;&gt;https://amanurrahman.com/blog-post/wordpress-website-migration-cost&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22970/wordpress-migration-what-it-actually-costs-and-how-not-to-lose-rankings</guid>
<pubDate>Sat, 18 Jul 2026 17:36:13 +0000</pubDate>
</item>
<item>
<title>SEO Migration Guide: Keep Rankings When Switching from WordPress</title>
<link>https://coderlegion.com/21106/seo-migration-guide-keep-rankings-when-switching-from-wordpress</link>
<description>
&lt;p&gt;&amp;lt;!-- ========== SCHEMAS ========== --&amp;gt;

&amp;lt;!-- FAQPage schema (generated from translation data) --&amp;gt;


&amp;lt;!-- WebSite schema --&amp;gt;


&amp;lt;!-- LocalBusiness schema --&amp;gt;



?&amp;gt;


&amp;lt;!-- Organization schema --&amp;gt;


&amp;lt;!-- BreadcrumbList schema (headline from translation) --&amp;gt;


&amp;lt;!-- CommunicateAction schema --&amp;gt;


&amp;lt;!-- ========== ARTICLE CONTENT ========== --&amp;gt;
&lt;/p&gt;&lt;h1&gt;SEO Migration Guide: Keep Your Rankings When You Switch Platforms&lt;/h1&gt;
&lt;div class=&quot;post-meta&quot;&gt;
    &lt;span class=&quot;post-date&quot;&gt;July 2025&lt;/span&gt;
    &lt;span class=&quot;post-read&quot;&gt;16 min read&lt;/span&gt;
&lt;/div&gt;

&lt;p class=&quot;intro&quot;&gt;&lt;/p&gt;&lt;p&gt;I&amp;rsquo;m Jacob Campbell, and I&amp;rsquo;ve migrated sites off WordPress without dropping a single ranking &amp;mdash; so let me walk you through how it&amp;rsquo;s actually done. Most &amp;ldquo;we lost all our traffic after the redesign&amp;rdquo; horror stories come from a handful of avoidable mistakes: broken redirects, changed URLs, and missing metadata. Done properly, a migration is invisible to Google. This guide gives you the exact sequence, grounded in Google&amp;rsquo;s own site-move documentation, so your rankings come along for the ride.&lt;/p&gt;

&lt;h2&gt;&lt;/h2&gt;
&lt;ul&gt;
    &lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h3&gt;&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
    &lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h3&gt;&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h3&gt;&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h3&gt;&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h3&gt;&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
    &lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h3&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;&lt;/strong&gt; &amp;ndash; &lt;/p&gt;

&lt;h2&gt;&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
    &lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
    &lt;/ul&gt;

&lt;h2&gt;&lt;/h2&gt;
&lt;ol&gt;
    &lt;/ol&gt;

&lt;h2&gt;&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h3&gt;&lt;/h3&gt;
&lt;ul&gt;
    &lt;/ul&gt;

&lt;h3&gt;&lt;/h3&gt;
&lt;ul&gt;
    &lt;/ul&gt;

&lt;h3&gt;&lt;/h3&gt;
&lt;ul&gt;
    &lt;/ul&gt;

&lt;h2&gt;&lt;/h2&gt;
&lt;ul&gt;
    &lt;/ul&gt;

&lt;h2&gt;&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
    &lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
    &lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;&lt;/h2&gt;
&lt;ul&gt;
    &lt;/ul&gt;

&lt;h2&gt;&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;

&lt;div class=&quot;article-cta&quot;&gt;
    &lt;a href=&quot;/contact&quot;&gt;&lt;/a&gt;
&lt;/div&gt;

&lt;p&gt;&lt;small&gt;&lt;em&gt;&lt;/em&gt;&lt;/small&gt;&lt;/p&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/21106/seo-migration-guide-keep-rankings-when-switching-from-wordpress</guid>
<pubDate>Sun, 05 Jul 2026 07:45:45 +0000</pubDate>
</item>
<item>
<title>Adobe Commerce now costs $40k/year — is Magento Open Source worth it?</title>
<link>https://coderlegion.com/18928/adobe-commerce-now-costs-40k-year-is-magento-open-source-worth-it</link>
<description>&lt;p&gt;Adobe Commerce Cloud licensing has crossed $40k/year for many mid-market merchants in 2026, and the cost-benefit math is shifting fast. We migrated Adobe Cloud to Magento Open Source over the last 14 months — here's the technical reality behind the marketing version of this story.&lt;/p&gt;
&lt;h3 id=&quot;when-the-math-actually-flips&quot;&gt;When the math actually flips&lt;/h3&gt;
&lt;p&gt;The cost case for Magento Open Source is real, but it's also misleading without context. License savings of $22k–40k/year only matter if you weren't using the Adobe-only features that came with that license.&lt;/p&gt;
&lt;p&gt;In our three migrations, the breakdown looked like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Store 1&lt;/strong&gt; (apparel, ~$1.8M GMV): used B2B Suite heavily → migration required custom replacement module, ~6 weeks dev work&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Store 2&lt;/strong&gt; (jewellery, ~$2.4M GMV): used only core features + Page Builder → straightforward, 8 weeks total&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Store 3&lt;/strong&gt; (electronics, ~$3.1M GMV): used Adobe Sensei for product recommendations → replaced with self-hosted ML, complex but cheaper long-term&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The actual lesson: audit your Adobe-only feature usage BEFORE running cost projections. Most merchants pay for things they don't use, but some pay for things they critically depend on.&lt;/p&gt;
&lt;h3 id=&quot;what-stays-the-same&quot;&gt;What stays the same&lt;/h3&gt;
&lt;p&gt;Both platforms share the same Magento 2 core. The framework, module structure, GraphQL schema, and admin layout are identical. If you've worked in Magento 2 for years, nothing about your day-to-day changes after migration.&lt;/p&gt;
&lt;p&gt;What stays:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Database schema (mostly — see below)&lt;/li&gt;
&lt;li&gt;Module APIs and DI patterns&lt;/li&gt;
&lt;li&gt;Frontend structure (Luma, Hyvä, headless all work the same)&lt;/li&gt;
&lt;li&gt;Composer-based dependency management&lt;/li&gt;
&lt;li&gt;Magento CLI commands&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-actually-breaks&quot;&gt;What actually breaks&lt;/h3&gt;
&lt;p&gt;This is where most posts get hand-wavy. Here are the real technical issues we hit:&lt;/p&gt;
&lt;h4 id=&quot;1-database-tables-with-row-id-vs-entity-id&quot;&gt;1. Database tables with &lt;code&gt;row_id&lt;/code&gt; vs &lt;code&gt;entity_id&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Adobe Commerce uses &lt;code&gt;row_id&lt;/code&gt; in catalog tables to support staging/preview features. Open Source uses &lt;code&gt;entity_id&lt;/code&gt;. If you have custom modules that query catalog tables directly (instead of through the Repository pattern), they will silently break.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Adobe Commerce
SELECT * FROM catalog_product_entity WHERE row_id = ?

-- Magento Open Source
SELECT * FROM catalog_product_entity WHERE entity_id = ?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We found ~40 places across three projects where developers had bypassed the Repository pattern. Each one needed manual fixing.&lt;/p&gt;
&lt;h4 id=&quot;2-b2b-suite-dependencies&quot;&gt;2. B2B Suite dependencies&lt;/h4&gt;
&lt;p&gt;Adobe's B2B Suite is deeply integrated — company accounts, shared catalogs, quote workflows, requisition lists. If you used it, you're either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rebuilding it from scratch (~8-12 weeks)&lt;/li&gt;
&lt;li&gt;Using a paid alternative (Aitoc, Wyomind, Magexperts have partial coverage)&lt;/li&gt;
&lt;li&gt;Removing the functionality entirely&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There's no clean export-import path. We've seen this kill migration projects in week 2.&lt;/p&gt;
&lt;h4 id=&quot;3-adobe-sensei-product-recommendations&quot;&gt;3. Adobe Sensei product recommendations&lt;/h4&gt;
&lt;p&gt;This one surprised us. The recommendation widgets aren't just disabled after migration — they leave orphaned database tables and broken admin pages. Cleanup script needed.&lt;/p&gt;
&lt;p&gt;Replacement options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Self-hosted ML (we've used Bloomreach, Algolia Recommend)&lt;/li&gt;
&lt;li&gt;Open source modules (mgt-commerce/related-products, amasty/recommendations)&lt;/li&gt;
&lt;li&gt;Rebuild on Magento's native related products engine&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None are drop-in. Budget 2-4 weeks.&lt;/p&gt;
&lt;h4 id=&quot;4-page-builder-content&quot;&gt;4. Page Builder content&lt;/h4&gt;
&lt;p&gt;Page Builder content is stored as JSON. Open Source has Page Builder too, so most content loads — but some Adobe-Commerce-only widgets (like dynamic blocks tied to customer segments) render as empty divs. Audit your CMS pages before go-live.&lt;/p&gt;
&lt;h4 id=&quot;5-hosting-migration&quot;&gt;5. Hosting migration&lt;/h4&gt;
&lt;p&gt;Adobe Cloud uses a specific Fastly + Platform.sh setup. When you move to alternatives like Hypernode, MGT Commerce, or Cloudways, you need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rebuild Fastly VCL (or move to Varnish)&lt;/li&gt;
&lt;li&gt;Reconfigure cron jobs (Adobe Cloud auto-provisions, others don't)&lt;/li&gt;
&lt;li&gt;Re-implement deploy hooks (&lt;code&gt;.magento.app.yaml&lt;/code&gt; → your CI/CD)&lt;/li&gt;
&lt;li&gt;Move secrets out of Adobe's variables system&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Plan for ~2 weeks of DevOps work, not 2 days.&lt;/p&gt;
&lt;h3 id=&quot;cost-comparison-the-honest-version&quot;&gt;Cost comparison — the honest version&lt;/h3&gt;
&lt;p&gt;For a mid-market store (~$2-3M GMV), 5-year cost picture:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Adobe Commerce Cloud (current):&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Item &lt;/th&gt;&lt;th&gt;Cost&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;License ($30k/yr × 5) &lt;/td&gt;&lt;td&gt;$150,000&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Hosting &lt;/td&gt;&lt;td&gt;included&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Adobe support &lt;/td&gt;&lt;td&gt;included&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;$150,000&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Magento Open Source (post-migration):&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Item &lt;/th&gt;&lt;th&gt;Cost&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;License &lt;/td&gt;&lt;td&gt;$0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Hosting ($8k/yr × 5) &lt;/td&gt;&lt;td&gt;$40,000&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Dev partner / in-house ($20k/yr × 5) &lt;/td&gt;&lt;td&gt;$100,000&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;One-time migration &lt;/td&gt;&lt;td&gt;$40,000-80,000&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;$180,000-220,000&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;So the savings aren't always there. They show up when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You already have an in-house Magento dev (or trusted partner)&lt;/li&gt;
&lt;li&gt;You use only core features&lt;/li&gt;
&lt;li&gt;You want infrastructure choices Adobe Cloud doesn't allow&lt;/li&gt;
&lt;li&gt;You're projecting beyond year 5 (license is $0 forever, infrastructure amortises)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;when-to-migrate-when-to-stay&quot;&gt;When to migrate, when to stay&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Migrate if:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;License is $25k+/year and you use ≤30% of Adobe-only features&lt;/li&gt;
&lt;li&gt;You need infrastructure flexibility (Hyvä, custom regions, specific cache layers)&lt;/li&gt;
&lt;li&gt;Your team or agency has senior Magento experience&lt;/li&gt;
&lt;li&gt;You're already planning a major replatform/redesign&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Stay if:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You depend heavily on B2B Suite or Sensei&lt;/li&gt;
&lt;li&gt;Your team doesn't have DevOps capacity for self-managed infrastructure&lt;/li&gt;
&lt;li&gt;You need Adobe's SLA for compliance reasons&lt;/li&gt;
&lt;li&gt;Your license is bundled with other Adobe products you actively use&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-decision-framework&quot;&gt;The decision framework&lt;/h3&gt;
&lt;p&gt;For each Adobe-only feature you use, ask:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What does it cost to replace?&lt;/li&gt;
&lt;li&gt;What does it cost to NOT have it?&lt;/li&gt;
&lt;li&gt;Is the replacement maintained?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you can't answer those three for every feature, you're not ready to migrate.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you've gone through this migration recently — what was the gotcha you didn't expect? I'm collecting war stories for a deeper write-up on the database schema edge cases specifically.&lt;/p&gt;
&lt;p&gt;Full breakdown with cost tables and FAQ on the original: &lt;a rel=&quot;nofollow&quot; href=&quot;https://angeo.dev/migrating-from-adobe-commerce-cloud-to-magento-open-source-is-it-worth-it/&quot;&gt;https://angeo.dev/migrating-from-adobe-commerce-cloud-to-magento-open-source-is-it-worth-it/&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18928/adobe-commerce-now-costs-40k-year-is-magento-open-source-worth-it</guid>
<pubDate>Mon, 25 May 2026 19:52:07 +0000</pubDate>
</item>
<item>
<title>OpenClaw Database Designer Skill: Comprehensive Guide</title>
<link>https://coderlegion.com/13092/openclaw-database-designer-skill-comprehensive-guide</link>
<description>
&lt;p&gt;&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;
&lt;h1&gt;OpenClaw Database Designer Skill: Comprehensive Guide&lt;/h1&gt;
&lt;p&gt;The OpenClaw Database Designer skill is a &lt;strong&gt;POWERFUL Tier&lt;/strong&gt; database design tool that provides expert-level analysis, optimization, and migration capabilities for modern database systems. This comprehensive skill combines theoretical principles with practical tools to help architects and developers create scalable, performant, and maintainable database schemas.&lt;/p&gt;
&lt;h2&gt;Core Competencies&lt;/h2&gt;
&lt;h3&gt;Schema Design &amp;amp; Analysis&lt;/h3&gt;
&lt;p&gt;The skill offers advanced schema design capabilities including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Normalization Analysis&lt;/strong&gt;: Automated detection of normalization levels from 1NF through BCNF&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Denormalization Strategy&lt;/strong&gt;: Smart recommendations for performance optimization&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Type Optimization&lt;/strong&gt;: Identification of inappropriate types and size issues&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Constraint Analysis&lt;/strong&gt;: Missing foreign keys, unique constraints, and null checks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Naming Convention Validation&lt;/strong&gt;: Consistent table and column naming patterns&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ERD Generation&lt;/strong&gt;: Automatic Mermaid diagram creation from DDL&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Index Optimization&lt;/h3&gt;
&lt;p&gt;Advanced index analysis and optimization features include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Index Gap Analysis&lt;/strong&gt;: Identification of missing indexes on foreign keys and query patterns&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composite Index Strategy&lt;/strong&gt;: Optimal column ordering for multi-column indexes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Index Redundancy Detection&lt;/strong&gt;: Elimination of overlapping and unused indexes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance Impact Modeling&lt;/strong&gt;: Selectivity estimation and query cost analysis&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Index Type Selection&lt;/strong&gt;: B-tree, hash, partial, covering, and specialized indexes&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Migration Management&lt;/h3&gt;
&lt;p&gt;The skill provides comprehensive migration capabilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Zero-Downtime Migrations&lt;/strong&gt;: Expand-contract pattern implementation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Schema Evolution&lt;/strong&gt;: Safe column additions, deletions, and type changes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Migration Scripts&lt;/strong&gt;: Automated data transformation and validation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rollback Strategy&lt;/strong&gt;: Complete reversal capabilities with validation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Execution Planning&lt;/strong&gt;: Ordered migration steps with dependency resolution&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Database Design Principles&lt;/h2&gt;
&lt;p&gt;The skill incorporates industry best practices for database design:&lt;/p&gt;
&lt;h3&gt;Schema Design Best Practices&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Use meaningful names: Clear, consistent naming conventions&lt;/li&gt;
&lt;li&gt;Choose appropriate data types: Right-sized columns for storage efficiency&lt;/li&gt;
&lt;li&gt;Define proper constraints: Foreign keys, check constraints, unique indexes&lt;/li&gt;
&lt;li&gt;Consider future growth: Plan for scale from the beginning&lt;/li&gt;
&lt;li&gt;Document relationships: Clear foreign key relationships and business rules&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Performance Optimization&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Index strategically: Cover common query patterns without over-indexing&lt;/li&gt;
&lt;li&gt;Monitor query performance: Regular analysis of slow queries&lt;/li&gt;
&lt;li&gt;Partition large tables: Improve query performance and maintenance&lt;/li&gt;
&lt;li&gt;Use appropriate isolation levels: Balance consistency with performance&lt;/li&gt;
&lt;li&gt;Implement connection pooling: Efficient resource utilization&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Security Considerations&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Principle of least privilege: Grant minimal necessary permissions&lt;/li&gt;
&lt;li&gt;Encrypt sensitive data: At rest and in transit&lt;/li&gt;
&lt;li&gt;Audit access patterns: Monitor and log database access&lt;/li&gt;
&lt;li&gt;Validate inputs: Prevent SQL injection attacks&lt;/li&gt;
&lt;li&gt;Regular security updates: Keep database software current&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Practical Applications&lt;/h2&gt;
&lt;p&gt;The Database Designer skill is invaluable for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Database architects designing new systems&lt;/li&gt;
&lt;li&gt;Developers optimizing existing schemas&lt;/li&gt;
&lt;li&gt;DevOps engineers managing database migrations&lt;/li&gt;
&lt;li&gt;Database administrators monitoring performance&lt;/li&gt;
&lt;li&gt;Development teams ensuring best practices&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Benefits&lt;/h2&gt;
&lt;p&gt;Using this skill provides numerous advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Improved database performance through optimized schemas&lt;/li&gt;
&lt;li&gt;Reduced maintenance costs with automated analysis&lt;/li&gt;
&lt;li&gt;Enhanced scalability through proper design principles&lt;/li&gt;
&lt;li&gt;Increased reliability with constraint validation&lt;/li&gt;
&lt;li&gt;Better documentation through automatic ERD generation&lt;/li&gt;
&lt;li&gt;Streamlined migration processes with zero-downtime capabilities&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Effective database design requires balancing multiple competing concerns: performance, scalability, maintainability, and business requirements. The OpenClaw Database Designer skill provides the tools and knowledge to make informed decisions throughout the database lifecycle, from initial schema design through production optimization and evolution.&lt;/p&gt;
&lt;p&gt;The included tools automate common analysis and optimization tasks, while the comprehensive guides provide the theoretical foundation for making sound architectural decisions. Whether building a new system or optimizing an existing one, these resources provide expert-level guidance for creating robust, scalable database solutions.&lt;/p&gt;
&lt;p&gt;By leveraging this skill, organizations can ensure their database designs are not only functional but also optimized for performance, scalability, and maintainability, ultimately leading to more successful and sustainable database systems.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;Skill can be found at: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/openclaw/skills/tree/main/skills/alirezarezvani/database-designer/SKILL.md&quot;&gt;https://github.com/openclaw/skills/tree/main/skills/alirezarezvani/database-designer/SKILL.md&lt;/a&gt;&lt;/p&gt;&lt;p&gt;The post &lt;a rel=&quot;nofollow&quot; href=&quot;https://insightginie.com/openclaw-database-designer-skill-comprehensive-guide/&quot;&gt;OpenClaw Database Designer Skill: Comprehensive Guide&lt;/a&gt; first appeared on &lt;a rel=&quot;nofollow&quot; href=&quot;https://insightginie.com&quot;&gt;Insight Ginie&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/13092/openclaw-database-designer-skill-comprehensive-guide</guid>
<pubDate>Sat, 21 Mar 2026 14:15:43 +0000</pubDate>
</item>
<item>
<title>Migration challenges are Infinate! Why Data Migration is a Major Engineering Challenge.</title>
<link>https://coderlegion.com/12972/migration-challenges-are-infinate-why-data-migration-is-a-major-engineering-challenge</link>
<description>&lt;h2 id=&quot;why-data-migration-is-still-a-major-engineering-challenge-even-when-you-control-both-endpoints-amp-how-data-in-place-architectures-like-hammer-space-reduce-the-intensity-of-problem-&quot;&gt;Why Data Migration Is Still a Major Engineering Challenge — Even When You Control Both Endpoints &amp;amp; How Data‑In‑Place Architectures Like Hammer space reduce the intensity of problem.&lt;/h2&gt;
&lt;p&gt;Enterprises often assume that migration should be easy because they “own” both the source and the destination. In reality, migration remains one of the most expensive, risky, and time‑consuming operations in data engineering. The difficulty has nothing to do with copying bytes — it comes from the state, semantics, governance, and operational constraints surrounding the data.&lt;/p&gt;
&lt;p&gt;Below is my technical explanation and please keep note that it is not complete.&lt;/p&gt;
&lt;h3 id=&quot;1-source-and-destination-are-never-truly-equivalent-even-when-both-systems-are-under-your-control-they-differ-in-&quot;&gt;1. Source and Destination Are Never Truly Equivalent; Even when both systems are under your control, they differ in:&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-code&quot;&gt;Storage Semantics
Block vs file vs object
POSIX vs non‑POSIX
Strong vs eventual consistency
Snapshot semantics
Versioning behavior (S3 vs NetApp vs Azure Blob)
Metadata Models
ACLs
Extended attributes
Ownership
Timestamps
Object tags
Retention policies
Schema &amp;amp; Serialization
Parquet, ORC, Avro, JSON, CSV, XML
Different compression codecs
Different type systems (timestamp precision, decimal scale)
Different partitioning strategies
Migration must reconcile all of these differences — and that reconciliation is non‑trivial.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2-migration-must-preserve-state-not-just-data&quot;&gt;2. Migration Must Preserve State, Not Just Data&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-code&quot;&gt;Copying bytes is easy.
Preserving state is hard.
A correct migration must preserve:
Permissions
Ownership
ACLs
Timestamps
Directory structure
Object metadata
Version history
Snapshots
Checksums
Referential integrity
Application‑level invariants
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is why rsync, robocopy, SnapMirror, and S3 sync have dozens of flags — because state preservation is complex.&lt;/p&gt;
&lt;h3 id=&quot;3-data-mutates-during-migration-drift-problem-this-is-the-biggest-technical-reason-migration-is-painful-&quot;&gt;3. Data Mutates During Migration (Drift Problem); This is the biggest technical reason migration is painful.&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-code&quot;&gt;While you are copying:
New files are created
Existing files are modified
Deletes occur
Partial writes happen
Transactions commit mid‑transfer
This forces you to implement:
Change Data Capture (CDC)
Incremental deltas
Reconciliation logic
Conflict resolution
Idempotent copy operations
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Drift is worse during migration than during first‑time ingestion, because now you must keep two systems synchronized until cutover.&lt;/p&gt;
&lt;h3 id=&quot;4-validation-at-scale-is-expensive&quot;&gt;4. Validation at Scale Is Expensive&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-code&quot;&gt;Copying 50TB is trivial.
Proving that 50TB is correct is not.
Validation requires:
Hashing (MD5/SHA‑256)
Byte‑level comparison
Metadata comparison
Permission comparison
Object count reconciliation
Schema validation
Referential integrity checks
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At petabyte scale, validation becomes a multi‑day operation.&lt;/p&gt;
&lt;h3 id=&quot;5-migration-must-respect-operational-constraints&quot;&gt;5. Migration Must Respect Operational Constraints&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-code&quot;&gt;Network Constraints
Throughput limits
Latency
Packet loss
Cross‑region bandwidth caps
Cloud throttling
Production Constraints
we cannot saturate the network
we cannot pause applications
we cannot break SLAs
we cannot degrade latency‑sensitive workloads
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Migration must run without impacting production, which forces throttling, scheduling, and multi‑phase execution.&lt;/p&gt;
&lt;h3 id=&quot;6-governance-compliance-and-residency-rules-still-apply&quot;&gt;6. Governance, Compliance, and Residency Rules Still Apply&lt;/h3&gt;
&lt;p&gt;Even if you “own” the data, you must comply with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-code&quot;&gt;GDPR
HIPAA
PCI‑DSS
SOC2
Data residency
Encryption requirements
Key management policies
Retention policies
Legal hold
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;migration-must-preserve-&quot;&gt;Migration must preserve:&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-code&quot;&gt;Encryption at rest
Encryption in transit
Key rotation
Access logs
Audit trails
Every copy multiplies governance burden.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;7-migration-creates-secondary-problems&quot;&gt;7. Migration Creates Secondary Problems&lt;/h3&gt;
&lt;p&gt;Storage Footprint Explosion&lt;/p&gt;
&lt;p&gt;Our temporarily hold:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-code&quot;&gt;Source
Destination
Snapshots
Backups
Validation copies
Copy Proliferation
Dev wants a copy
QA wants a copy
Data science wants a copy
AI workloads want a copy
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;each-copy-requires-&quot;&gt;Each copy requires:&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-code&quot;&gt;Access control
Masking
Lineage
Governance
Synchronisation Loops
Until cut over, you must keep:
deltas
CDC streams
logs
metadata updates in sync.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;8-migration-is-a-workflow-not-a-copy-operation&quot;&gt;8. Migration Is a Workflow, Not a Copy Operation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-code&quot;&gt;A real migration includes:
Discovery
Classification
Governance mapping
Schema mapping
Capacity planning
Network planning
Incremental copy
Validation
Drift detection
Reconciliation
Cut over
Rollback plan
Decommissioning
Each step has failure modes.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;9-how-hammerspace-s-data-in-place-architecture-eliminates-migration&quot;&gt;9. How Hammerspace’s Data‑In‑Place Architecture Eliminates Migration&lt;/h2&gt;
&lt;p&gt;This is where your attached document becomes critical.&lt;/p&gt;
&lt;p&gt;Hammerspace’s AI Data Platform (AIDP) eliminates migration by introducing:&lt;/p&gt;
&lt;h3 id=&quot;9-1-global-namespace-layer&quot;&gt;9.1 Global Namespace Layer&lt;/h3&gt;
&lt;p&gt;Instead of:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-code&quot;&gt;NetApp → COPY → AI Storage
S3     → COPY → AI Storage
Azure  → COPY → AI Storage
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;we get:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-code&quot;&gt;NetApp ─┐
S3     ─┼─→ Global Namespace → AI Workloads
Azure  ─┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No migration.&lt;br&gt;
No duplication.&lt;br&gt;
No drift.&lt;br&gt;
No re‑validation.&lt;/p&gt;
&lt;h3 id=&quot;9-2-mcp-server-model-context-protocol-&quot;&gt;9.2 MCP Server (Model Context Protocol)&lt;/h3&gt;
&lt;p&gt;The MCP server handles:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-code&quot;&gt;Natural language queries
Governance validation (via Secuvy DSPM)
Data discovery
Data orchestration
GPU staging
Pipeline transparency
This replaces 10–15 separate tools.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;9-3-local-rtx-gpus-for-pre-processing&quot;&gt;9.3 Local RTX GPUs for Pre‑Processing&lt;/h3&gt;
&lt;h4 id=&quot;traditional-workflow-wastes-h100-h200-cycles-on-cleaning-redaction-formatting-validation-&quot;&gt;Traditional workflow wastes H100/H200 cycles on: Cleaning, Redaction, Formatting, Validation.&lt;/h4&gt;
&lt;h2 id=&quot;hammerspace-offloads-all-of-this-to-local-rtx-gpus-so-training-gpus-only-train-&quot;&gt;Hammerspace offloads all of this to local RTX GPUs, so training GPUs only train.&lt;/h2&gt;
&lt;h3 id=&quot;9-4-no-rip-and-replace&quot;&gt;9.4 No Rip‑and‑Replace&lt;/h3&gt;
&lt;h4 id=&quot;existing-tools-airflow-dbt-mlflow-run-ai-etc-continue-working-they-simply-point-to-the-global-namespace-&quot;&gt;Existing tools (Airflow, dbt, MLflow, Run:AI, etc.) continue working. They simply point to the global namespace.&lt;/h4&gt;
&lt;h3 id=&quot;9-5-automated-governance-amp-data-refresh&quot;&gt;9.5 Automated Governance &amp;amp; Data Refresh&lt;/h3&gt;
&lt;h4 id=&quot;continuous-discovery-continuous-governance-validation-automated-lineage-automated-audit-trails-&quot;&gt;Continuous discovery, Continuous governance validation, Automated lineage, Automated audit trails,&lt;/h4&gt;
&lt;h3 id=&quot;9-6-multi-cloud-multi-site-multi-vendor&quot;&gt;9.6 Multi‑Cloud, Multi‑Site, Multi‑Vendor&lt;/h3&gt;
&lt;h4 id=&quot;data-stays-where-it-is-ai-workloads-go-to-the-data-not-the-other-way-around-&quot;&gt;Data stays where it is. AI workloads go to the data. Not the other way around.&lt;/h4&gt;
&lt;h2 id=&quot;final-humble-technical-summary&quot;&gt;Final Humble Technical Summary&lt;/h2&gt;
&lt;p&gt;Migration is difficult because:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-code&quot;&gt;Data is not static
State must be preserved
Systems are not symmetric
Validation is expensive
Governance is mandatory
Networks are limited
Production cannot be disrupted
Drift continues during migration
Synchronisation is required until cut over
Hammerspace eliminates these problems by:
Creating a global namespace
Using data in place
Applying governance centrally
Offloading pre-processing to local GPUs
Providing transparent orchestration
Integrating with existing tools
Avoiding duplication and drift
Reducing time to production from 16–30 weeks to ~4 weeks
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the technical reason migration is a big deal — and why data‑in‑place architectures are the future of enterprise AI.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12972/migration-challenges-are-infinate-why-data-migration-is-a-major-engineering-challenge</guid>
<pubDate>Tue, 17 Mar 2026 01:54:25 +0000</pubDate>
</item>
<item>
<title>Building a Kubernetes Migration Framework: Lessons from Ingress-NGINX</title>
<link>https://coderlegion.com/12246/building-a-kubernetes-migration-framework-lessons-from-ingress-nginx</link>
<description>
&lt;a href=&quot;https://alexandre-vazquez.com/building-a-kubernetes-migration-framework-lessons-from-ingress-nginx/&quot; title=&quot;Building a Kubernetes Migration Framework: Lessons from Ingress-NGINX&quot; rel=&quot;nofollow&quot;&gt;&lt;img width=&quot;1536&quot; height=&quot;1024&quot; src=&quot;https://mlk5tkvgfeoa.i.optimole.com/cb:s8WW.68/w:1536/h:1024/q:mauto/f:best/https://alexandre-vazquez.com/wp-content/uploads/2026/02/k8s-ingress-migration.avif&quot; class=&quot;webfeedsFeaturedVisual wp-post-image&quot; alt=&quot;Building a Kubernetes Migration Framework: Lessons from Ingress-NGINX&quot; style=&quot;display: block; margin: auto; margin-bottom: 5px;max-width: 100%;&quot; title=&quot;Building a Kubernetes Migration Framework: Lessons from Ingress-NGINX 4&quot;&gt;&lt;/a&gt;
&lt;p&gt;The recent announcement regarding the &lt;a href=&quot;https://www.reddit.com/r/kubernetes/comments/1qw534a/understanding_the_ingressnginx_deprecation_before/&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;deprecation of the Ingress-NGINX controller&lt;/a&gt; sent a ripple through the Kubernetes community. For many organizations, it&amp;rsquo;s the first major deprecation of a foundational, widely-adopted ecosystem component. While the immediate reaction is often tactical&amp;mdash;&amp;rdquo;What do we replace it with?&amp;rdquo;&amp;mdash;the more valuable long-term question is strategic: &amp;ldquo;How do we systematically manage this and future migrations?&amp;rdquo;&lt;/p&gt;



&lt;p&gt;This event isn&amp;rsquo;t an anomaly; it&amp;rsquo;s a precedent. As Kubernetes matures, core add-ons, APIs, and patterns will evolve or sunset. Platform engineering teams need a repeatable, low-risk framework for navigating these changes. Drawing from the Ingress-NGINX transition and established deployment management principles, we can abstract a robust &lt;strong&gt;Kubernetes Migration Framework&lt;/strong&gt; applicable to any major component, from service meshes to CSI drivers.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Why Ad-Hoc Migrations Fail in Production&lt;/h2&gt;



&lt;p&gt;Attempting a &amp;ldquo;big bang&amp;rdquo; replacement or a series of manual, one-off changes is a recipe for extended downtime, configuration drift, and undetected regression. Production Kubernetes environments are complex systems with deep dependencies:&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;
&lt;li&gt;&lt;strong&gt;Interdependent Workloads:&lt;/strong&gt; Multiple applications often share the same ingress controller, relying on specific annotations, custom snippets, or behavioral quirks.&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Automation and GitOps Dependencies:&lt;/strong&gt; Helm charts, Kustomize overlays, and ArgoCD/Flux manifests are tightly coupled to the existing component&amp;rsquo;s API and schema.&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Observability and Security Integration:&lt;/strong&gt; Monitoring dashboards, logging parsers, and security policies are tuned for the current implementation.&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Knowledge Silos:&lt;/strong&gt; Tribal knowledge about workarounds and specific configurations isn&amp;rsquo;t documented.&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;A structured framework mitigates these risks by enforcing discipline, creating clear validation gates, and ensuring the capability to roll back at any point.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;The Four-Phase Kubernetes Migration Framework&lt;/h2&gt;



&lt;p&gt;This framework decomposes the migration into four distinct phases: Assessment, Parallel Run, Cutover, and Decommission. Each phase has defined inputs, activities, and exit criteria.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Phase 1: Deep Assessment &amp;amp; Dependency Mapping&lt;/h3&gt;



&lt;p&gt;Before writing a single line of new configuration, understand the full scope. The goal is to move from &amp;ldquo;we use Ingress-NGINX&amp;rdquo; to a precise inventory of &lt;em&gt;how&lt;/em&gt; it&amp;rsquo;s used.&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;
&lt;li&gt;&lt;strong&gt;Inventory All Ingress Resources:&lt;/strong&gt; Use &lt;code&gt;kubectl get ingress --all-namespaces&lt;/code&gt; as a starting point, but go deeper.&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Analyze Annotation Usage:&lt;/strong&gt; Script an analysis to catalog every annotation in use (e.g., &lt;code&gt;nginx.ingress.kubernetes.io/rewrite-target&lt;/code&gt;, &lt;code&gt;nginx.ingress.kubernetes.io/configuration-snippet&lt;/code&gt;). This reveals functional dependencies.&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Map to Backend Services:&lt;/strong&gt; For each Ingress, identify the backend Services and Namespaces. This highlights critical applications and potential blast radius.&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Review Customizations:&lt;/strong&gt; Document any custom ConfigMaps for main NGINX configuration, custom template patches, or modifications to the controller deployment itself.&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Evaluate Alternatives:&lt;/strong&gt; Based on the inventory, evaluate candidate replacements (e.g., &lt;a rel=&quot;nofollow&quot; href=&quot;https://alexandre-vazquez.com/kubernetes-gateway-api-versions-compatibility-guide/&quot; data-type=&quot;post&quot; data-id=&quot;6712&quot;&gt;Gateway API&lt;/a&gt; with a compatible implementation, another Ingress controller like &lt;a href=&quot;https://github.com/emissary-ingress/emissary&quot; data-type=&quot;link&quot; data-id=&quot;https://github.com/emissary-ingress/emissary&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;Emissary-ingress&lt;/a&gt; or Traefik). The &lt;a href=&quot;https://docs.cloud.google.com/architecture/migrating-containers-kubernetes-gke&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;Google Cloud migration framework&lt;/a&gt; provides a useful decision tree for ingress-specific migrations.&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;The output of this phase is a migration manifesto: a concrete list of what needs to be converted, grouped by complexity and criticality.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Phase 2: Phased Rollout &amp;amp; Parallel Run&lt;/h3&gt;



&lt;p&gt;This is the core of a low-risk migration. Instead of replacing, you run the new and old systems in parallel, shifting traffic gradually. For ingress, this often means installing the new controller alongside the old one.&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;
&lt;li&gt;&lt;strong&gt;Dual Installation:&lt;/strong&gt; Deploy the new ingress controller in the same cluster, configured with a distinct ingress class (e.g., &lt;code&gt;ingressClassName: gateway&lt;/code&gt; vs. &lt;code&gt;nginx&lt;/code&gt;).&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Create Canary Ingress Resources:&lt;/strong&gt; For a low-risk application, create a parallel Ingress or Gateway resource pointing to the new controller. Use techniques like &lt;a href=&quot;https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;managed deployments with canary patterns&lt;/a&gt; to control exposure.&lt;br&gt;&lt;pre&gt;&lt;code&gt;# Example: A new Gateway API HTTPRoute for a canary service&lt;br&gt;apiVersion: gateway.networking.k8s.io/v1&lt;br&gt;kind: HTTPRoute&lt;br&gt;metadata:&lt;br&gt;  name: app-canary&lt;br&gt;spec:&lt;br&gt;  parentRefs:&lt;br&gt;  - name: company-gateway&lt;br&gt;  rules:&lt;br&gt;  - backendRefs:&lt;br&gt;    - name: app-service&lt;br&gt;      port: 8080&lt;br&gt;      weight: 10 # Start with 10% of traffic&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Validate Equivalency:&lt;/strong&gt; Use traffic mirroring (if supported) or direct synthetic testing against both ingress paths. Compare logs, response headers, latency, and error rates.&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Iterate and Expand:&lt;/strong&gt; Gradually increase traffic weight or add more applications to the new stack, group by group, based on the assessment from Phase 1.&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;This phase relies heavily on your observability stack. Dashboards comparing error rates, latency (p50, p99), and throughput between the old and new paths are essential.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Phase 3: Validation &amp;amp; Automated Cutover&lt;/h3&gt;



&lt;p&gt;The cutover is not a manual event. It&amp;rsquo;s the final step in a validation process.&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;
&lt;li&gt;&lt;strong&gt;Define Validation Tests:&lt;/strong&gt; Create a suite of tests that must pass before full cutover. This includes:
&lt;ul class=&quot;wp-block-list&quot;&gt;
&lt;li&gt;Smoke tests for all critical user journeys.&lt;/li&gt;



&lt;li&gt;Load tests to verify performance under expected traffic patterns.&lt;/li&gt;



&lt;li&gt;Security scan validation (e.g., no unintended ports open).&lt;/li&gt;



&lt;li&gt;Compliance checks (e.g., specific headers are present).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Automate the Switch:&lt;/strong&gt; For each application, the cutover is ultimately a change in its Ingress or Gateway resource. This should be done via your GitOps pipeline. Update the source manifests (e.g., change the &lt;code&gt;ingressClassName&lt;/code&gt;), merge, and let automation apply it. This ensures the state is declarative and recorded.&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Maintain Rollback Capacity:&lt;/strong&gt; The old system must remain operational and routable (with reduced capacity) during this phase. The GitOps rollback is simply reverting the manifest change.&lt;/li&gt;
&lt;/ul&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Phase 4: Observability &amp;amp; Decommission&lt;/h3&gt;



&lt;p&gt;Once all traffic is successfully migrated and validated over a sustained period (e.g., 72 hours), you can decommission the old component.&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;
&lt;li&gt;&lt;strong&gt;Monitor Aggressively:&lt;/strong&gt; Keep a close watch on all key metrics for at least one full business cycle (a week).&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Remove Old Resources:&lt;/strong&gt; Delete the old controller&amp;rsquo;s Deployment, Service, ConfigMaps, and CRDs (if no longer needed).&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Clean Up Auxiliary Artifacts:&lt;/strong&gt; Remove old RBAC bindings, service accounts, and any custom monitoring alerts or dashboards specific to the old component.&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Document Lessons Learned:&lt;/strong&gt; Update runbooks and architecture diagrams. Note any surprises, gaps in the process, or validation tests that were particularly valuable.&lt;/li&gt;
&lt;/ul&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Key Principles for a Resilient Framework&lt;/h2&gt;



&lt;p&gt;Beyond the phases, these principles should guide your framework&amp;rsquo;s design:&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;
&lt;li&gt;&lt;strong&gt;Always Maintain Rollback Capability:&lt;/strong&gt; Every step should be reversible with minimal disruption. This is a core tenet of &lt;a href=&quot;https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;managing Kubernetes deployments&lt;/a&gt;.&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Leverage GitOps for State Management:&lt;/strong&gt; All desired state changes (Ingress resources, controller deployments) must flow through version-controlled manifests. This provides an audit trail, consistency, and the simplest rollback mechanism (git revert).&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Validate with Production Traffic Patterns:&lt;/strong&gt; Synthetic tests are insufficient. Use canary weights and traffic mirroring to validate with real user traffic in a controlled manner.&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Communicate Transparently:&lt;/strong&gt; Platform teams should maintain a clear migration status page for internal stakeholders, showing which applications have been migrated, which are in progress, and the overall timeline.&lt;/li&gt;
&lt;/ul&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Conclusion: Building a Migration-Capable Platform&lt;/h2&gt;



&lt;p&gt;The deprecation of Ingress-NGINX is a wake-up call. The next major change is a matter of &amp;ldquo;when,&amp;rdquo; not &amp;ldquo;if.&amp;rdquo; By investing in a structured migration framework now, platform teams transform a potential crisis into a manageable, repeatable operational procedure.&lt;/p&gt;



&lt;p&gt;This framework&amp;mdash;&lt;strong&gt;Assess, Run in Parallel, Validate, and Decommission&lt;/strong&gt;&amp;mdash;abstracts the specific lessons from the ingress migration into a generic pattern. It can be applied to migrating from PodSecurityPolicies to Pod Security Standards, from a deprecated CSI driver, or from one service mesh to another. The tools (GitOps, canary deployments, observability) are already in your stack. The value is in stitching them together into a disciplined process that ensures platform evolution doesn&amp;rsquo;t compromise platform stability.&lt;/p&gt;



&lt;p&gt;Start by documenting this framework as a runbook template. Then, apply it to your next significant component update, even a minor one, to refine the process. When the next major deprecation announcement lands in your inbox, you&amp;rsquo;ll be ready.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/12246/building-a-kubernetes-migration-framework-lessons-from-ingress-nginx</guid>
<pubDate>Sat, 28 Feb 2026 05:30:04 +0000</pubDate>
</item>
<item>
<title>Improving my previous OpenRewrite recipe</title>
<link>https://coderlegion.com/3367/improving-my-previous-openrewrite-recipe</link>
<description>&lt;p&gt;I started discovering OpenRewrite last week by writing a Kotlin recipe that moves Kotlin files according to the official directory structure recommendation. I mentioned some future works, and here they are. In this post, I want to describe how to compute the root package instead of letting the user set it.&lt;/p&gt;
&lt;h3 id=&quot;reminder&quot;&gt;Reminder&lt;/h3&gt;
&lt;p&gt;I developed last week a recipe to follow the Kotlin recommendation regarding directory structure:&lt;/p&gt;
&lt;p&gt;&amp;gt;In pure Kotlin projects, the recommended directory structure follows the package structure with the common root package omitted. For example, if all the code in the project is in the &lt;code&gt;org.example.kotlin&lt;/code&gt; package and its subpackages, files with the &lt;code&gt;org.example.kotlin&lt;/code&gt; package should be placed directly under the source root, and files in &lt;code&gt;org.example.kotlin.network.socket&lt;/code&gt; should be in the network/socket subdirectory of the source root.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&amp;gt;-- &lt;a rel=&quot;nofollow&quot; href=&quot;https://kotlinlang.org/docs/coding-conventions.html#source-code-organization&quot;&gt;Directory structure&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In a Java project, if you have packages &lt;code&gt;ch.frankel.foo&lt;/code&gt;, &lt;code&gt;ch.frankel.bar&lt;/code&gt;, and &lt;code&gt;ch.frankel.baz&lt;/code&gt;, you'll get the following structure:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src
 |__ main
    |__ java
       |__ ch
          |__ frankel
             |__ foo
             |__ bar
             |__ baz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Kotlin projects, many developers follow the same structure as above, but it can be flattened as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src
 |__ main
    |__ kotlin
       |__ foo
       |__ bar
       |__ baz
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-work&quot;&gt;The work&lt;/h3&gt;
&lt;p&gt;My recipe's original version mandated that you configure the root package yourself, &lt;em&gt;e.g.&lt;/em&gt;, &lt;code&gt;ch.frankel&lt;/code&gt; for the above example. However, it should be possible to compute it automatically, from looking at the source files. It adds an extra step to the process: before moving the file to the root, the recipe should look at each source file, get the package, compute the longest prefix with the existing root, make it the root, and go to the next source file. The regular &lt;code&gt;Recipe&lt;/code&gt; doesn't work in this case. We need to switch to a &lt;code&gt;ScanningRecipe&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&amp;gt;If a recipe needs to generate new source files or needs to see all source files before making changes, it must be a &lt;code&gt;ScanningRecipe&lt;/code&gt;. A &lt;code&gt;ScanningRecipe&lt;/code&gt; extends the normal &lt;code&gt;Recipe&lt;/code&gt; and adds two key objects: an &lt;code&gt;accumulator&lt;/code&gt; and a &lt;code&gt;scanner&lt;/code&gt;. The &lt;code&gt;accumulator&lt;/code&gt; object is a custom data structure defined by the recipe itself to store any information the recipe needs to function. The &lt;code&gt;scanner&lt;/code&gt; object is a visitor which populates the &lt;code&gt;accumulator&lt;/code&gt; with data.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&amp;gt;-- &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.openrewrite.org/concepts-and-explanations/recipes#scanning-recipes&quot;&gt;Scanning recipes&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Scanning recipes offer two steps: the first to gather data, the second to do the work.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=14758325757968137092&quot; alt=&quot;ScanningRecipe class diagram&quot;&gt;&lt;/p&gt;
&lt;p&gt;We must design our algorithm within the constraints of OpenRewrite, and they are the following: in the first phase, for each source file, OpenRewrite will call the &lt;code&gt;getScanner()&lt;/code&gt; method that returns a visitor of our choice. In turn, OpenRewrite calls the visitor's methods, which can access the accumulator.&lt;/p&gt;
&lt;p&gt;My first naive approach was to use a collection as the accumulator, but it's not necessary. The algorithm is much simpler if we set a mutable placeholder that holds the package root and update it if necessary during each visit. The initial value should be &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the value is &lt;code&gt;null&lt;/code&gt;, which happens on the first visitor, set the package root to the source file's package.&lt;/li&gt;
&lt;li&gt;If the value is an empty string, skip–see below.&lt;/li&gt;
&lt;li&gt;In any other case, compute the new package root by finding the longest prefix between the existing package root and the source file's package.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It might result in an empty string, indicating that packages have no common root, &lt;em&gt;e.g.&lt;/em&gt;, &lt;code&gt;ch.frankel.foo&lt;/code&gt; and &lt;code&gt;org.frankel.foo&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here's the updated code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class FlattenStructure(private val rootPackage: String?) : ScanningRecipe&amp;lt;AtomicReference&amp;lt;String?&amp;gt;&amp;gt;() { //1-2

  constructor() : this(null)                                                   //3

  override fun getDisplayName(): String = &quot;Flatten Kotlin package directory structure&quot;
  override fun getDescription(): String =
    &quot;Move Kotlin files to match idiomatic layout by omitting the root package according to the official recommendation.&quot;

  override fun getInitialValue(ctx: ExecutionContext) = AtomicReference&amp;lt;String?&amp;gt;(null) //4

  override fun getScanner(acc: AtomicReference&amp;lt;String?&amp;gt;): TreeVisitor&amp;lt;*, ExecutionContext&amp;gt; {
    if (rootPackage != null) return TreeVisitor.noop&amp;lt;Tree, ExecutionContext&amp;gt;() //5
    return object : KotlinIsoVisitor&amp;lt;ExecutionContext&amp;gt;() {
      override fun visitCompilationUnit(cu: K.CompilationUnit, ctx: ExecutionContext): K.CompilationUnit {
        val packageName = cu.packageDeclaration?.packageName ?: return cu
        val computedPackage = acc.get()
        when (computedPackage) {
          null -&amp;gt; acc.set(packageName)                                         //6
          &quot;&quot; -&amp;gt; {}                                                             //7
          else -&amp;gt; {
            val commonPrefix = packageName.commonPrefixWith(computedPackage).removeSuffix(&quot;.&quot;) //8
            acc.set(commonPrefix)
          }
        }
        return cu
      }
    }
  }

  override fun getVisitor(acc: AtomicReference&amp;lt;String?&amp;gt;): TreeVisitor&amp;lt;*, ExecutionContext&amp;gt; {
    return object : KotlinIsoVisitor&amp;lt;ExecutionContext&amp;gt;() {
      override fun visitCompilationUnit(cu: K.CompilationUnit, ctx: ExecutionContext): K.CompilationUnit {
        val packageName = cu.packageDeclaration?.packageName ?: return cu
        val packageToSet: String? = rootPackage ?: acc.get()                   //9
        if (packageToSet == null || packageToSet.isEmpty()) return cu
        val relativePath = packageName.removePrefix(packageToSet).removePrefix(&quot;.&quot;)
          .replace('.', '/')
        val filename = cu.sourcePath.fileName.toString()
        val newPath: Path = Paths.get(&quot;src/main/kotlin&quot;)
          .resolve(relativePath)
          .resolve(filename)
        return cu.withSourcePath(newPath)
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Inherit from &lt;code&gt;ScanningRecipe&lt;/code&gt; instead of directly from &lt;code&gt;Recipe&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set the accumulator type to be an &lt;code&gt;AtomicReference&amp;lt;String?&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Make the configuration easier when you don't override the package root&lt;/li&gt;
&lt;li&gt;The initial root is uninitialized&lt;/li&gt;
&lt;li&gt;Skip the computation if the root package is manually set&lt;/li&gt;
&lt;li&gt;If it's the first file visited, the accumulator holds &lt;code&gt;null&lt;/code&gt;, and we can set the (temporary) root as the current package&lt;/li&gt;
&lt;li&gt;One of the previous computations returned no common root–do nothing&lt;/li&gt;
&lt;li&gt;Find the longest common prefix between the held package root and the current package&lt;/li&gt;
&lt;li&gt;The only difference with the original code:&lt;br&gt;
we check if the root package has been set manually otherwise, we use the one computed in the first pass&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;optimizing-the-recipe&quot;&gt;Optimizing the recipe&lt;/h3&gt;
&lt;p&gt;You may have noticed that when there is no common root, &lt;em&gt;e.g.&lt;/em&gt;, &lt;code&gt;ch.frankel.foo&lt;/code&gt; and &lt;code&gt;org.frankel.foo&lt;/code&gt;, we scan all files anyway. In a small codebase, it's not a big issue, but when scanning millions of source files, that's a huge waste of CPU cycles and time. If you run the recipe in the Cloud, it directly translates to money. We should stop scanning as soon as we detect the computed package root is an empty string to optimize the recipe.&lt;/p&gt;
&lt;p&gt;Here's the updated code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;override fun getScanner(acc: AtomicReference&amp;lt;String?&amp;gt;): TreeVisitor&amp;lt;*, ExecutionContext&amp;gt; {
  if (rootPackage != null) return TreeVisitor.noop&amp;lt;Tree, ExecutionContext&amp;gt;()   //1
  val currentPackage = acc.get()
  if (currentPackage == &quot;&quot;) return TreeVisitor.noop&amp;lt;Tree, ExecutionContext&amp;gt;()  //2
  return object : KotlinIsoVisitor&amp;lt;ExecutionContext&amp;gt;() {
    override fun visitCompilationUnit(cu: K.CompilationUnit, ctx: ExecutionContext): K.CompilationUnit {
      val packageName = cu.packageDeclaration?.packageName ?: return cu
      // Different call than the one above!
      val currentPackage = acc.get()
      // First scanned file
      if (currentPackage == null) acc.set(packageName)                         //3
      else {
        // Find the longest common prefix between the stored package and the current one
        val commonPrefix = packageName.commonPrefixWith(currentPackage).removeSuffix(&quot;.&quot;)
        acc.set(commonPrefix)
      }
      return cu
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;If the root package has been set, skip visiting&lt;/li&gt;
&lt;li&gt;If one of the previous computations sets an empty string, there isn't any common root package: skip visiting&lt;/li&gt;
&lt;li&gt;Simplify the visitor by removing the clause when the accumulator is an empty string since it can't happen anymore&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Note that OpenRewrite still scans each file, but at least doesn't visit it thanks to the no-op visitor.&lt;/p&gt;
&lt;h3 id=&quot;counting-visits&quot;&gt;Counting visits&lt;/h3&gt;
&lt;p&gt;I made a couple of attempts before finding the right approach to the above. To ensure that I got it right, I wanted to display the number of visits by the scanner. We can use the accumulator to increment the visit count. Here are the changes I made:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Migrated from an &lt;code&gt;AtomicReference&amp;lt;String?&amp;gt;&lt;/code&gt; to an &lt;code&gt;AtomicReference&amp;lt;Pair&amp;lt;Int, String?&amp;gt;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;getInitialValue()&lt;/code&gt; function returns &lt;code&gt;AtomicReference&amp;lt;Pair&amp;lt;Int, String?&amp;gt;&amp;gt;(0 to null)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;During each visit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Get the visits count from the accumulator&lt;/li&gt;
&lt;li&gt;Increment it&lt;/li&gt;
&lt;li&gt;Print it&lt;/li&gt;
&lt;li&gt;Store it back in the accumulator&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Update the tests accordingly&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With packages &lt;code&gt;ch.frankel.blog.foo&lt;/code&gt;, &lt;code&gt;org.frankel.blog.bar&lt;/code&gt;, and &lt;code&gt;org.frankel.blog.baz&lt;/code&gt;, the log shows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[INFO] [stdout] FlattenStructure: 1 file visited
[INFO] [stdout] FlattenStructure: 2 files visited
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By changing &lt;code&gt;ch.frankel.blog.foo&lt;/code&gt; to &lt;code&gt;org.frankel.blog.foo&lt;/code&gt;, the log changes to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[INFO] [stdout] FlattenStructure: 1 files visited
[INFO] [stdout] FlattenStructure: 2 files visited
[INFO] [stdout] FlattenStructure: 3 files visited
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because I made these changes just to validate my understanding of how OpenRewrite works, I put them in the &lt;code&gt;visits_count&lt;/code&gt; branch on GitHub. To see the differences, execute &lt;code&gt;git diff master visits_count&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;In this post, I added the automatic computation of the root package. I had to change my design and understand how scanning recipes work. Then, I skipped further visits when there wasn't any common root package to optimize performance.&lt;/p&gt;
&lt;p&gt;The recipe is still not serializable, though it's a recommendation. I also noticed that my tests didn't leverage OpenRewrite's testing API. There's still a lot of work to do!&lt;/p&gt;
&lt;p&gt;The complete source code for this post can be found on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ajavageek/flatten-kotlin-recipe&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To go further:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.openrewrite.org/concepts-and-explanations/recipes#scanning-recipes&quot;&gt;Scanning Recipes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/openrewrite/rewrite-testing-frameworks/blob/main/src/main/java/org/openrewrite/java/testing/mockito/AnyToNullable.java&quot;&gt;Code sample of an existing scanning recipe&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.openrewrite.org/reference/scanning-recipes&quot;&gt;Existing scanning recipes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/openrewrite-recipes/2/&quot;&gt;A Java Geek&lt;/a&gt; on June 15&lt;sup&gt;th&lt;/sup&gt;, 2025&lt;/em&gt;&lt;/p&gt;
</description>
<category>Java</category>
<guid isPermaLink="true">https://coderlegion.com/3367/improving-my-previous-openrewrite-recipe</guid>
<pubDate>Thu, 19 Jun 2025 09:26:47 +0000</pubDate>
</item>
</channel>
</rss>