<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aleksandr Gusev</title>
    <description>The latest articles on DEV Community by Aleksandr Gusev (@aleksandr_gusev_it).</description>
    <link>https://dev.to/aleksandr_gusev_it</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3722273%2F5243199d-577f-4828-82c5-b39bc42d9257.jpg</url>
      <title>DEV Community: Aleksandr Gusev</title>
      <link>https://dev.to/aleksandr_gusev_it</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aleksandr_gusev_it"/>
    <language>en</language>
    <item>
      <title>Fabric.js Wasn't the Problem. Our Architecture Was.</title>
      <dc:creator>Aleksandr Gusev</dc:creator>
      <pubDate>Wed, 30 Sep 2026 11:04:48 +0000</pubDate>
      <link>https://dev.to/aleksandr_gusev_it/fabricjs-wasnt-the-problem-our-architecture-was-1boa</link>
      <guid>https://dev.to/aleksandr_gusev_it/fabricjs-wasnt-the-problem-our-architecture-was-1boa</guid>
      <description>&lt;p&gt;We build an interactive office map.&lt;/p&gt;

&lt;p&gt;There is a floor plan in the background, desks, meeting rooms, parking spaces, lockers, points of interest, and pods — groups of desks that can move together.&lt;/p&gt;

&lt;p&gt;Admins drag objects around, rotate them, change their shapes, print the floor plan, and save drafts.&lt;/p&gt;

&lt;p&gt;The canvas is built with &lt;strong&gt;Fabric.js 5&lt;/strong&gt;. The application around it is &lt;strong&gt;Angular 19&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And for a long time, Fabric.js wasn't just the rendering layer.&lt;/p&gt;

&lt;p&gt;It was basically the architecture.&lt;/p&gt;

&lt;p&gt;That was the problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Old Map Was a Fabric Application That Happened to Have Entities
&lt;/h2&gt;

&lt;p&gt;In the old implementation, a &lt;code&gt;DeskPresenter&lt;/code&gt; extended &lt;code&gt;fabric.Group&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its constructor took &lt;code&gt;entityData.viewConfig&lt;/code&gt; and passed properties such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;left&lt;/span&gt;
&lt;span class="nx"&gt;top&lt;/span&gt;
&lt;span class="nx"&gt;scaleX&lt;/span&gt;
&lt;span class="nx"&gt;angle&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;directly into Fabric.&lt;/p&gt;

&lt;p&gt;The presenter kept a reference to the &lt;code&gt;Desk&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Desk&lt;/code&gt; kept a reference back to the presenter.&lt;/p&gt;

&lt;p&gt;So instead of having:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Domain Entity
      +
Canvas Object
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we effectively had:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Desk
  ↕
DeskPresenter
  ↕
Fabric.Group
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The distinction between our domain model and the canvas object became increasingly blurry.&lt;/p&gt;

&lt;p&gt;It was convenient at first.&lt;/p&gt;

&lt;p&gt;Then the side effects started appearing.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Fabric Became Too Global
&lt;/h2&gt;

&lt;p&gt;We had a &lt;code&gt;FabricExtension&lt;/code&gt; that modified &lt;code&gt;fabric.Object.prototype&lt;/code&gt; at application startup.&lt;/p&gt;

&lt;p&gt;It added things like custom corners, caching behavior, &lt;code&gt;snapAngle&lt;/code&gt;, &lt;code&gt;centeredRotation&lt;/code&gt;, and custom rendering.&lt;/p&gt;

&lt;p&gt;There was also older map code modifying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;fabric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setCursor&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and even adding functionality to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;CanvasRenderingContext2D&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some custom selection controls lived on those prototypes too.&lt;/p&gt;

&lt;p&gt;That means these changes weren't limited to the office map.&lt;/p&gt;

&lt;p&gt;They were effectively &lt;strong&gt;process-global&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Open another screen that happened to use Fabric and it could inherit behavior that was originally created specifically for the office map.&lt;/p&gt;

&lt;p&gt;That was a pretty good sign that the boundaries weren't where they should be.&lt;/p&gt;




&lt;h2&gt;
  
  
  Events Had the Same Problem
&lt;/h2&gt;

&lt;p&gt;The old AngularJS implementation also had a &lt;code&gt;ToParentEventsConnector&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its job was to translate Fabric-related events into &lt;code&gt;$scope&lt;/code&gt; events.&lt;/p&gt;

&lt;p&gt;The class had a large switch over things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DeskHoveredEvent
ParkingMouseOutEvent
CreateDragBookingEvent
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every new map object could mean another event class and another branch.&lt;/p&gt;

&lt;p&gt;Again, Fabric events, application events, and AngularJS communication were tightly connected.&lt;/p&gt;

&lt;p&gt;The map wasn't just rendering objects.&lt;/p&gt;

&lt;p&gt;It was becoming responsible for communicating with the rest of the application.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why We Kept Fabric.js
&lt;/h1&gt;

&lt;p&gt;We did consider whether rebuilding the rendering layer from scratch would give us a cleaner architecture.&lt;/p&gt;

&lt;p&gt;We decided not to.&lt;/p&gt;

&lt;p&gt;Fabric already solves many difficult problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;object selection;&lt;/li&gt;
&lt;li&gt;transformations;&lt;/li&gt;
&lt;li&gt;zooming;&lt;/li&gt;
&lt;li&gt;hit testing;&lt;/li&gt;
&lt;li&gt;object caching;&lt;/li&gt;
&lt;li&gt;grouping;&lt;/li&gt;
&lt;li&gt;rotation;&lt;/li&gt;
&lt;li&gt;serialization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our desks aren't simple rectangles either.&lt;/p&gt;

&lt;p&gt;They're assembled from multiple paths and wrapped in Fabric groups.&lt;/p&gt;

&lt;p&gt;Pods are also groups of desk presenters.&lt;/p&gt;

&lt;p&gt;Reimplementing all of that with raw Canvas would mean spending our time rebuilding functionality that Fabric already handles well.&lt;/p&gt;

&lt;p&gt;So the decision became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Keep Fabric. Stop letting Fabric define the application architecture.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That was the reason for introducing &lt;code&gt;MapEngine&lt;/code&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  Introducing MapEngine
&lt;/h1&gt;

&lt;p&gt;The new architecture puts an application-level boundary between Angular and Fabric.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Angular
   │
   ▼
MapEditor
   │
   ▼
MapEngine
   ├── MapStage
   ├── MapViewport
   ├── ObjectRegistry
   └── ChangesTracker
   │
   ▼
Fabric.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The responsibilities are deliberately separated.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MapStage&lt;/code&gt; owns the Fabric canvas.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MapViewport&lt;/code&gt; owns the camera.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ObjectRegistry&lt;/code&gt; maps application entities to their canvas representations.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ChangesTracker&lt;/code&gt; tracks what needs to be saved.&lt;/p&gt;

&lt;p&gt;That separation turned out to be much more important than simply moving code into smaller files.&lt;/p&gt;




&lt;h1&gt;
  
  
  MapStage Is the Canvas
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;MapStage&lt;/code&gt; owns the actual Fabric canvas.&lt;/p&gt;

&lt;p&gt;It deals with things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;adding objects;&lt;/li&gt;
&lt;li&gt;removing objects;&lt;/li&gt;
&lt;li&gt;canvas size;&lt;/li&gt;
&lt;li&gt;rendering;&lt;/li&gt;
&lt;li&gt;active objects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It doesn't need to know how the application thinks about zoom.&lt;/p&gt;

&lt;p&gt;That's the viewport's job.&lt;/p&gt;

&lt;p&gt;This distinction sounds small, but it gives us a useful rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The canvas renders. The engine decides what the canvas means.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  The Camera Is a Matrix
&lt;/h1&gt;

&lt;p&gt;One of the decisions I'm particularly happy with is how we handle panning and zooming.&lt;/p&gt;

&lt;p&gt;The tempting approach is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The user panned the map, so move every object."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We deliberately don't do that.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MapViewport&lt;/code&gt; works with Fabric's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;viewportTransform&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The objects stay in their original plan coordinates.&lt;/p&gt;

&lt;p&gt;The camera moves.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              Camera
                │
                ▼
        viewportTransform
                │
                ▼
        ┌───────────────┐
        │ Office Floor  │
        │               │
        │  Desk   Desk  │
        │       Room    │
        └───────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives us a useful separation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Object position = position on the floor

Viewport transform = what the user currently sees
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters later when we need to save, print, or calculate coordinates.&lt;/p&gt;

&lt;p&gt;The desk shouldn't suddenly have different coordinates just because the user zoomed to 200%.&lt;/p&gt;




&lt;h2&gt;
  
  
  Zoom Isn't Just &lt;code&gt;canvas.setZoom()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The viewport logic also has to deal with constraints.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;fitBackground()&lt;/code&gt; calculates the scale required to fit the floor plan.&lt;/p&gt;

&lt;p&gt;The editor allows zooming below that level, but only within a configured minimum.&lt;/p&gt;

&lt;p&gt;The maximum zoom is also limited.&lt;/p&gt;

&lt;p&gt;When zooming out, the viewport is gradually pulled back toward the center of the floor plan so the map doesn't drift away into empty space.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;relativePan()&lt;/code&gt; converts wheel movement into an absolute translation based on the current viewport state.&lt;/p&gt;

&lt;p&gt;And after zooming, the transform is normalized again so small numerical errors don't accumulate.&lt;/p&gt;

&lt;p&gt;None of this belongs in an Angular component.&lt;/p&gt;

&lt;p&gt;It belongs in the viewport.&lt;/p&gt;




&lt;h1&gt;
  
  
  Interaction Is Another Layer
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;PanInteraction&lt;/code&gt; sits on top of the viewport.&lt;/p&gt;

&lt;p&gt;The interaction rules are roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Wheel / trackpad
       ↓
      Pan

Ctrl/Cmd + wheel
       ↓
      Zoom

Space + drag
       ↓
      Pan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While space-dragging is active, we also disable target finding so the pointer doesn't accidentally select a desk while the user is trying to move the camera.&lt;/p&gt;

&lt;p&gt;That's another useful property of the architecture.&lt;/p&gt;

&lt;p&gt;The interaction logic doesn't need to know how a desk is rendered.&lt;/p&gt;

&lt;p&gt;It only needs to know what the viewport can do.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Registry Is a Pair, Not a Subclass
&lt;/h1&gt;

&lt;p&gt;One of the biggest changes was separating the application entity from the Fabric object.&lt;/p&gt;

&lt;p&gt;The new &lt;code&gt;ObjectRegistry&lt;/code&gt; stores:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;IMapCanvasObject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IEntityData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fabric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;label&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;fabric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IText&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's intentionally small.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;entity&lt;/code&gt; is our application data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id
type
name
assignment
viewConfig
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;presenter&lt;/code&gt; is the Fabric representation.&lt;/p&gt;

&lt;p&gt;The label is separate as well.&lt;/p&gt;

&lt;p&gt;So instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Desk = Fabric.Group
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we now have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Desk entity
    │
    └── Fabric presenter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a subtle change, but it gives us something very important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the domain object doesn't have to be a canvas object.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  ObjectRegistry Doesn't Know How to Draw a Desk
&lt;/h1&gt;

&lt;p&gt;The registry only provides operations such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;getById&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;removeById&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;getAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It doesn't know how to create a desk.&lt;/p&gt;

&lt;p&gt;That's handled by &lt;code&gt;MapObjectFactory&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The factory decides which Fabric representation belongs to a particular &lt;code&gt;SpaceType&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This keeps object creation away from the registry and keeps the registry away from rendering details.&lt;/p&gt;

&lt;p&gt;It's a small abstraction.&lt;/p&gt;

&lt;p&gt;But small abstractions are often the useful ones.&lt;/p&gt;




&lt;h1&gt;
  
  
  Serialization Is Where the Boundary Gets Interesting
&lt;/h1&gt;

&lt;p&gt;We still have to move data between the application model and Fabric.&lt;/p&gt;

&lt;p&gt;When an object changes, &lt;code&gt;serializeMapPresenter()&lt;/code&gt; reads properties such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;left
top
scale
angle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and writes them back into the entity's &lt;code&gt;viewConfig&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;ObjectModificationInteraction&lt;/code&gt; can notify the &lt;code&gt;ChangesTracker&lt;/code&gt; that the entity changed.&lt;/p&gt;

&lt;p&gt;This means the flow is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User moves desk
       ↓
Fabric event
       ↓
ObjectModificationInteraction
       ↓
serialize presenter
       ↓
update entity
       ↓
ChangesTracker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fabric is involved.&lt;/p&gt;

&lt;p&gt;But it isn't the source of truth for the whole application.&lt;/p&gt;

&lt;p&gt;That's the boundary we were missing before.&lt;/p&gt;




&lt;h1&gt;
  
  
  There Is Still Some Leakage
&lt;/h1&gt;

&lt;p&gt;The architecture isn't perfect.&lt;/p&gt;

&lt;p&gt;For example, presenters still contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mapObjectId&lt;/span&gt;
&lt;span class="nx"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mapObjectType&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those values are used to identify the application entity when a Fabric event arrives.&lt;/p&gt;

&lt;p&gt;It's a practical lookup key.&lt;/p&gt;

&lt;p&gt;But it's also a leak.&lt;/p&gt;

&lt;p&gt;Fabric still needs to know which application object generated an event.&lt;/p&gt;

&lt;p&gt;We haven't found a better way to completely remove that connection without making event handling more complicated than it needs to be.&lt;/p&gt;

&lt;p&gt;And that's okay.&lt;/p&gt;

&lt;p&gt;An abstraction doesn't have to hide every implementation detail.&lt;/p&gt;

&lt;p&gt;It just needs to hide the details that would otherwise spread everywhere.&lt;/p&gt;




&lt;h1&gt;
  
  
  ActiveSelection Instead of Group
&lt;/h1&gt;

&lt;p&gt;This was one of those small Fabric.js details that caused a surprisingly important architectural decision.&lt;/p&gt;

&lt;p&gt;Fabric normally turns multiple selected objects into a:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;fabric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Group&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We override that behavior and use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;fabric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ActiveSelection&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because a selection and a group are not the same thing in our domain.&lt;/p&gt;

&lt;p&gt;A group represents a real relationship between objects.&lt;/p&gt;

&lt;p&gt;A pod is a group of desks.&lt;/p&gt;

&lt;p&gt;A temporary selection of three desks is not a new domain object.&lt;/p&gt;

&lt;p&gt;If we turned the selection into a &lt;code&gt;Group&lt;/code&gt;, we'd be changing the structure of the map just because the user selected several objects.&lt;/p&gt;

&lt;p&gt;That's not what we want.&lt;/p&gt;

&lt;p&gt;So our &lt;code&gt;UnCanvas&lt;/code&gt; creates an &lt;code&gt;ActiveSelection&lt;/code&gt; instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;_createGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fabric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;groupObjects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_activeObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;fabric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ActiveSelection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;groupObjects&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;MAP_CONTROL_STYLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The selected objects remain independent.&lt;/p&gt;

&lt;p&gt;The selection is temporary.&lt;/p&gt;

&lt;p&gt;The domain model stays intact.&lt;/p&gt;




&lt;h1&gt;
  
  
  And Then There Are Coordinates
&lt;/h1&gt;

&lt;p&gt;This is another place where Fabric's internals become important.&lt;/p&gt;

&lt;p&gt;Inside an &lt;code&gt;ActiveSelection&lt;/code&gt;, the selected object's &lt;code&gt;left&lt;/code&gt; and &lt;code&gt;top&lt;/code&gt; are relative to the selection.&lt;/p&gt;

&lt;p&gt;Scale and rotation can also exist on the selection rather than directly on the desk.&lt;/p&gt;

&lt;p&gt;So before saving a modified object, we have to flatten the transform.&lt;/p&gt;

&lt;p&gt;Our implementation uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;calcTransformMatrix&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;qrDecompose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to extract the actual transform that belongs to the object.&lt;/p&gt;

&lt;p&gt;This is one of those problems that looks trivial until you have a real editor.&lt;/p&gt;

&lt;p&gt;The user sees:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I moved the desk."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fabric sees:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"A selection with a transformed child was modified."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The application needs to turn the second statement back into the first one.&lt;/p&gt;




&lt;h1&gt;
  
  
  ChangesTracker: Don't Save the Whole Map
&lt;/h1&gt;

&lt;p&gt;The new architecture also gives us a better place for change tracking.&lt;/p&gt;

&lt;p&gt;The old implementation already had a &lt;code&gt;ChangesTracker&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It maintained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;initialState
currentState

CREATE
UPDATE
DELETE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the idea of sending changes instead of the entire map wasn't new.&lt;/p&gt;

&lt;p&gt;What changed was the boundary.&lt;/p&gt;

&lt;p&gt;The canvas can change without becoming the application's source of truth.&lt;/p&gt;

&lt;p&gt;The flow is now conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User changes object
        ↓
Fabric
        ↓
MapEngine
        ↓
Entity updated
        ↓
ChangesTracker
        ↓
CREATE / UPDATE / DELETE
        ↓
Save draft
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes the save operation about &lt;strong&gt;what changed&lt;/strong&gt;, rather than about serializing everything currently sitting on the canvas.&lt;/p&gt;




&lt;h1&gt;
  
  
  Angular Doesn't Need to Know Fabric
&lt;/h1&gt;

&lt;p&gt;This is probably the biggest architectural improvement.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MapCanvasComponent&lt;/code&gt; owns a &lt;code&gt;MapEngine&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It can ask the engine to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;initialize;&lt;/li&gt;
&lt;li&gt;zoom;&lt;/li&gt;
&lt;li&gt;print;&lt;/li&gt;
&lt;li&gt;convert client coordinates to canvas coordinates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;MapEditorComponent&lt;/code&gt; owns the higher-level editor behavior.&lt;/p&gt;

&lt;p&gt;It creates the &lt;code&gt;ChangesTracker&lt;/code&gt;, hydrates it from the draft or published map, and attaches it to the engine.&lt;/p&gt;

&lt;p&gt;The editor can also provide callbacks for things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;opening an edit modal;&lt;/li&gt;
&lt;li&gt;confirming deletion;&lt;/li&gt;
&lt;li&gt;building a pod;&lt;/li&gt;
&lt;li&gt;updating sidebar state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fabric events stop at the engine boundary.&lt;/p&gt;

&lt;p&gt;Angular receives application-level information.&lt;/p&gt;

&lt;p&gt;That's the part we wanted.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Workspace Doesn't Need to Know About Fabric Either
&lt;/h1&gt;

&lt;p&gt;The editor has several workspace modes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Settings
Arrangement
Print
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each mode has different capabilities.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Arrangement
├── move
├── delete
├── group
└── add

Print
└── view only
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We resolve these capabilities separately and let the engine apply the required locks.&lt;/p&gt;

&lt;p&gt;That means we don't need to sprinkle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;print&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;through every presenter.&lt;/p&gt;

&lt;p&gt;The mode affects capabilities.&lt;/p&gt;

&lt;p&gt;It doesn't redefine the canvas architecture.&lt;/p&gt;




&lt;h1&gt;
  
  
  Was the New Architecture Perfect?
&lt;/h1&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;And that's probably the most useful part of the story.&lt;/p&gt;

&lt;p&gt;There are still places where Fabric concepts leak through.&lt;/p&gt;

&lt;p&gt;The presenter still carries an object ID.&lt;/p&gt;

&lt;p&gt;Serialization still has to understand Fabric transforms.&lt;/p&gt;

&lt;p&gt;Some legacy coordinate assumptions still exist.&lt;/p&gt;

&lt;p&gt;And the old map is still in the repository, which means the new implementation has to understand its coordinate language.&lt;/p&gt;

&lt;p&gt;But the important difference is that these problems now have a place to live.&lt;/p&gt;

&lt;p&gt;They don't automatically become problems for every Angular component.&lt;/p&gt;




&lt;h1&gt;
  
  
  What Actually Changed
&lt;/h1&gt;

&lt;p&gt;The biggest improvement wasn't fewer lines of code.&lt;/p&gt;

&lt;p&gt;It was the direction of dependencies.&lt;/p&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AngularJS
   ↕
Domain objects
   ↕
Fabric
   ↕
Events
   ↕
Services
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything knew about everything.&lt;/p&gt;

&lt;p&gt;Now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Angular
   ↓
MapEngine
   ↓
Fabric
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with the domain model and change tracking sitting behind explicit boundaries.&lt;/p&gt;

&lt;p&gt;That doesn't make the system magically simple.&lt;/p&gt;

&lt;p&gt;It makes complexity &lt;strong&gt;local&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And that is usually what we actually want from architecture.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;We didn't replace Fabric.js.&lt;/p&gt;

&lt;p&gt;We stopped asking Fabric.js to be our application architecture.&lt;/p&gt;

&lt;p&gt;That distinction turned out to be more important than the choice of canvas library itself.&lt;/p&gt;

&lt;p&gt;Fabric is very good at being a canvas engine.&lt;/p&gt;

&lt;p&gt;It doesn't need to know what a desk means to our business.&lt;/p&gt;

&lt;p&gt;It doesn't need to know how drafts are saved.&lt;/p&gt;

&lt;p&gt;It doesn't need to know what a workspace mode is.&lt;/p&gt;

&lt;p&gt;And Angular doesn't need to know how Fabric calculates a transform matrix.&lt;/p&gt;

&lt;p&gt;The goal of &lt;code&gt;MapEngine&lt;/code&gt; isn't to hide every Fabric API.&lt;/p&gt;

&lt;p&gt;It's to create a boundary where both sides can do what they're good at.&lt;/p&gt;

&lt;p&gt;We're still refining that boundary.&lt;/p&gt;

&lt;p&gt;But the map editor is now a system we can continue to evolve instead of a collection of UI code sitting directly on top of a canvas.&lt;/p&gt;

&lt;p&gt;And honestly, that's probably the biggest difference between the old map and the new one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is based on our experience building and maintaining a production interactive map editor. Code examples have been simplified or anonymized where necessary.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>angular</category>
      <category>architecture</category>
      <category>frontend</category>
      <category>software</category>
    </item>
    <item>
      <title>How We Migrated a Large AngularJS Application Without a Big-Bang Rewrite</title>
      <dc:creator>Aleksandr Gusev</dc:creator>
      <pubDate>Fri, 14 Aug 2026 11:39:53 +0000</pubDate>
      <link>https://dev.to/aleksandr_gusev_it/how-we-migrated-a-large-angularjs-application-without-a-big-bang-rewrite-498c</link>
      <guid>https://dev.to/aleksandr_gusev_it/how-we-migrated-a-large-angularjs-application-without-a-big-bang-rewrite-498c</guid>
      <description>&lt;p&gt;Migrating a large AngularJS application to modern Angular sounds simple on paper:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Rewrite the old application, move everything to Angular, remove AngularJS, and ship it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In a real production product, that's rarely an option.&lt;/p&gt;

&lt;p&gt;Our application has been running for years, new features still need to be delivered, and users don't care that we're in the middle of a framework migration. They expect the product to keep working.&lt;/p&gt;

&lt;p&gt;So instead of a Big-Bang rewrite, we chose a hybrid approach.&lt;/p&gt;

&lt;p&gt;Today, our application runs &lt;strong&gt;AngularJS 1.8.3 and Angular 19 side by side in a single application&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It's not a temporary demo.&lt;/p&gt;

&lt;p&gt;It's production.&lt;/p&gt;

&lt;p&gt;And roughly half of the application has already moved to Angular.&lt;/p&gt;

&lt;p&gt;This is what that migration actually looks like.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where We Started
&lt;/h2&gt;

&lt;p&gt;The legacy application is based on &lt;strong&gt;AngularJS 1.8.3&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The new application currently uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@angular/core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"19.2.14"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our target is Angular 19.&lt;/p&gt;

&lt;p&gt;Not "the latest Angular eventually".&lt;/p&gt;

&lt;p&gt;We deliberately chose a specific target version and are migrating toward it incrementally.&lt;/p&gt;

&lt;p&gt;The repository is large enough that the migration can't be described as simply replacing controllers with components.&lt;/p&gt;

&lt;p&gt;The rough picture today looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/js
└── ~1000 legacy files

src/ng-app
└── ~1500 Angular TS/HTML files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Angular side already contains around &lt;strong&gt;20 feature areas&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At this point, Angular isn't just an experiment sitting next to the legacy application.&lt;/p&gt;

&lt;p&gt;It's becoming the main platform.&lt;/p&gt;

&lt;p&gt;But AngularJS is still very much alive.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why We Didn't Rewrite Everything
&lt;/h1&gt;

&lt;p&gt;The obvious alternative was to create a new Angular application and gradually move everything into it.&lt;/p&gt;

&lt;p&gt;We decided against that.&lt;/p&gt;

&lt;p&gt;The problem wasn't just the amount of code.&lt;/p&gt;

&lt;p&gt;The application is a living product.&lt;/p&gt;

&lt;p&gt;While we're migrating the framework, the product still needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;new features;&lt;/li&gt;
&lt;li&gt;bug fixes;&lt;/li&gt;
&lt;li&gt;customer requests;&lt;/li&gt;
&lt;li&gt;integrations;&lt;/li&gt;
&lt;li&gt;analytics;&lt;/li&gt;
&lt;li&gt;improvements to existing workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stopping development for a year and saying "we'll come back when the rewrite is finished" isn't realistic.&lt;/p&gt;

&lt;p&gt;We needed the migration and product development to happen simultaneously.&lt;/p&gt;

&lt;p&gt;That led us to one principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;New functionality should be written in Angular, while legacy functionality can continue running until it is replaced.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This sounds obvious.&lt;/p&gt;

&lt;p&gt;Making it work is not.&lt;/p&gt;




&lt;h1&gt;
  
  
  One Application, Two Frameworks
&lt;/h1&gt;

&lt;p&gt;The most important architectural decision was to keep everything inside &lt;strong&gt;one hybrid application&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There is no iframe.&lt;/p&gt;

&lt;p&gt;There are no separate deployments.&lt;/p&gt;

&lt;p&gt;There isn't a second application running next to the old one.&lt;/p&gt;

&lt;p&gt;It's one application with two frameworks sharing the same runtime.&lt;/p&gt;

&lt;p&gt;The bridge is provided by Angular's &lt;code&gt;@angular/upgrade&lt;/code&gt; package.&lt;/p&gt;

&lt;p&gt;The simplified bootstrap process looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;platformBrowserDynamic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bootstrapModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AppModule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;platformRef&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upgrade&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;platformRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;injector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;UpgradeModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;upgrade&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unSpotApp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Angular starts first.&lt;/p&gt;

&lt;p&gt;AngularJS is then bootstrapped through &lt;code&gt;UpgradeModule&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This gives us a single application lifecycle while allowing both frameworks to coexist.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Router Is Still AngularJS
&lt;/h1&gt;

&lt;p&gt;This is one of the strangest parts of the migration.&lt;/p&gt;

&lt;p&gt;We have Angular 19.&lt;/p&gt;

&lt;p&gt;But our router is still &lt;strong&gt;UI-Router from the AngularJS side&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So the application doesn't have a clean boundary like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Legacy Application
        ↓
New Application
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, it's more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 UI-Router
                     │
          ┌──────────┴──────────┐
          │                     │
     AngularJS             Angular
     components            components
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The router still knows about the application's states.&lt;/p&gt;

&lt;p&gt;New Angular components are exposed to AngularJS using &lt;code&gt;downgradeComponent()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example, an Angular component can become something that the old router understands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ungLogin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;downgradeComponent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LoginComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then the state can reference it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ungLogin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the router's perspective, it is just another component.&lt;/p&gt;

&lt;p&gt;Underneath, it's Angular.&lt;/p&gt;

&lt;p&gt;This allowed us to move entire pages to Angular without immediately replacing the routing infrastructure.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Other Direction: Angular Calling AngularJS
&lt;/h1&gt;

&lt;p&gt;Migration would be much easier if Angular only needed to consume old functionality once.&lt;/p&gt;

&lt;p&gt;Unfortunately, real applications don't work that way.&lt;/p&gt;

&lt;p&gt;Angular sometimes needs legacy services.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;injector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are still AngularJS services that haven't been migrated yet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;$state&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;authService&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;legacy image cropping functionality;&lt;/li&gt;
&lt;li&gt;various existing application services.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So Angular can temporarily access the old dependency injection system.&lt;/p&gt;

&lt;p&gt;The important word here is &lt;strong&gt;temporarily&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  AngularJS Can Also Use Angular
&lt;/h1&gt;

&lt;p&gt;The dependency direction works both ways.&lt;/p&gt;

&lt;p&gt;We have new Angular services and state that legacy code needs to access.&lt;/p&gt;

&lt;p&gt;For example, the new application state is exposed back to AngularJS through &lt;code&gt;downgradeInjectable()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;providedIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;angular&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;module&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unSpotApp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ngAppState&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;downgradeInjectable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now legacy code can use the new Angular service.&lt;/p&gt;

&lt;p&gt;This creates an interesting situation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             Angular
                │
                │ downgradeInjectable
                ▼
          AngularJS code


          AngularJS
                │
                │ $injector
                ▼
             Angular
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both directions are possible.&lt;/p&gt;

&lt;p&gt;And that flexibility is what makes incremental migration possible.&lt;/p&gt;

&lt;p&gt;It's also one of the things that makes hybrid applications surprisingly difficult to reason about.&lt;/p&gt;




&lt;h1&gt;
  
  
  Two DI Containers
&lt;/h1&gt;

&lt;p&gt;One of the first things you learn in a hybrid application is that you effectively have &lt;strong&gt;two dependency injection worlds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Angular has its own DI system.&lt;/p&gt;

&lt;p&gt;AngularJS has &lt;code&gt;$injector&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;During migration, both have to coexist.&lt;/p&gt;

&lt;p&gt;Sometimes Angular uses a legacy service.&lt;/p&gt;

&lt;p&gt;Sometimes AngularJS consumes a new Angular service.&lt;/p&gt;

&lt;p&gt;Sometimes both implementations temporarily exist.&lt;/p&gt;

&lt;p&gt;That means dependency ownership becomes an architectural concern.&lt;/p&gt;

&lt;p&gt;We have to constantly ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which side owns this service now?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And more importantly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which side should own it after the migration?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This sounds like a small detail.&lt;/p&gt;

&lt;p&gt;Across a large codebase, it isn't.&lt;/p&gt;




&lt;h1&gt;
  
  
  State: One Truth, Two Worlds
&lt;/h1&gt;

&lt;p&gt;We introduced a new application state based on RxJS.&lt;/p&gt;

&lt;p&gt;Angular consumes it directly.&lt;/p&gt;

&lt;p&gt;But some of the initial information still originates in AngularJS.&lt;/p&gt;

&lt;p&gt;For example, after authentication, the legacy application already knows about the company.&lt;/p&gt;

&lt;p&gt;Instead of rewriting the entire authentication flow immediately, AngularJS pushes the information into the new state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ngAppState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setCompanySettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;$rootScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;company&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is effectively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 Login
                   │
                   ▼
              AngularJS
                   │
                   │ company settings
                   ▼
               AppState
                   │
                   ▼
                Angular
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one of the compromises we intentionally accepted.&lt;/p&gt;

&lt;p&gt;For now, we have two worlds and a manually maintained bridge between them.&lt;/p&gt;

&lt;p&gt;It's not beautiful.&lt;/p&gt;

&lt;p&gt;But it works.&lt;/p&gt;

&lt;p&gt;And more importantly, it lets us migrate the consumers before migrating the producer.&lt;/p&gt;




&lt;h1&gt;
  
  
  Two Translation Systems
&lt;/h1&gt;

&lt;p&gt;Internationalization created another similar problem.&lt;/p&gt;

&lt;p&gt;The legacy application uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;angular-translate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Angular application uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ngx-translate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So during migration, we have two translation systems running at the same time.&lt;/p&gt;

&lt;p&gt;Again, the long-term goal is obvious.&lt;/p&gt;

&lt;p&gt;We want one system.&lt;/p&gt;

&lt;p&gt;But rewriting every translation dependency wasn't necessary to start migrating pages.&lt;/p&gt;

&lt;p&gt;So we allowed the old system to remain where it was needed while new Angular features use the new implementation.&lt;/p&gt;

&lt;p&gt;This is one of the recurring patterns throughout the migration:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't migrate infrastructure just because it is old. Migrate it when the application boundary requires it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  What Has Already Been Migrated
&lt;/h1&gt;

&lt;p&gt;The Angular side is no longer a small part of the application.&lt;/p&gt;

&lt;p&gt;Several production areas have already moved to Angular, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;login;&lt;/li&gt;
&lt;li&gt;analytics;&lt;/li&gt;
&lt;li&gt;employee schedules;&lt;/li&gt;
&lt;li&gt;visitors;&lt;/li&gt;
&lt;li&gt;requests;&lt;/li&gt;
&lt;li&gt;buildings;&lt;/li&gt;
&lt;li&gt;users;&lt;/li&gt;
&lt;li&gt;organizational structure;&lt;/li&gt;
&lt;li&gt;integrations;&lt;/li&gt;
&lt;li&gt;check-in;&lt;/li&gt;
&lt;li&gt;office settings;&lt;/li&gt;
&lt;li&gt;displays;&lt;/li&gt;
&lt;li&gt;services;&lt;/li&gt;
&lt;li&gt;parts of the administration area.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;New features are also being developed in Angular.&lt;/p&gt;

&lt;p&gt;At the same time, some important parts are still AngularJS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the application shell;&lt;/li&gt;
&lt;li&gt;header;&lt;/li&gt;
&lt;li&gt;employee map;&lt;/li&gt;
&lt;li&gt;parts of the administration area;&lt;/li&gt;
&lt;li&gt;office map legacy functionality;&lt;/li&gt;
&lt;li&gt;schedules;&lt;/li&gt;
&lt;li&gt;desks;&lt;/li&gt;
&lt;li&gt;meeting rooms;&lt;/li&gt;
&lt;li&gt;lockers;&lt;/li&gt;
&lt;li&gt;inbox;&lt;/li&gt;
&lt;li&gt;space and group administration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the migration isn't:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AngularJS → Angular
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's currently closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AngularJS  ───────────────┐
                          │
                          ▼
                     Hybrid App
                          ▲
                          │
Angular  ────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The balance is roughly &lt;strong&gt;50/50&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Angular is no longer a side project.&lt;/p&gt;

&lt;p&gt;But AngularJS is still the owner of some fundamental parts of the application.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Biggest Problem: Living in the Hybrid
&lt;/h1&gt;

&lt;p&gt;The hardest part of the migration hasn't been Angular 19 itself.&lt;/p&gt;

&lt;p&gt;It's the hybrid architecture.&lt;/p&gt;

&lt;p&gt;Every time we touch a feature, we have to understand which world we're working in.&lt;/p&gt;

&lt;p&gt;Is the service Angular or AngularJS?&lt;/p&gt;

&lt;p&gt;Is the state coming from the old application or the new one?&lt;/p&gt;

&lt;p&gt;Which translation service should be used?&lt;/p&gt;

&lt;p&gt;Which router API is available?&lt;/p&gt;

&lt;p&gt;Can this component be converted directly?&lt;/p&gt;

&lt;p&gt;Does the legacy code expect an AngularJS object?&lt;/p&gt;

&lt;p&gt;Can Angular safely depend on this old service?&lt;/p&gt;

&lt;p&gt;The framework migration becomes less about syntax and more about &lt;strong&gt;managing boundaries&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Router Problem
&lt;/h1&gt;

&lt;p&gt;We still have UI-Router from the AngularJS world.&lt;/p&gt;

&lt;p&gt;There is even a &lt;code&gt;LegacyRouterService&lt;/code&gt; in the new application with a TODO that basically says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TODO: remove after the new router is introduced&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That comment has been around while the migration continues.&lt;/p&gt;

&lt;p&gt;It's a good reminder that migrations are rarely linear.&lt;/p&gt;

&lt;p&gt;Sometimes the cleanest architecture is not the architecture you build immediately.&lt;/p&gt;

&lt;p&gt;It's the architecture that lets you move from the old architecture to the new one without stopping the business.&lt;/p&gt;




&lt;h1&gt;
  
  
  The TypeScript Problem
&lt;/h1&gt;

&lt;p&gt;There is another interesting side effect of running a modern Angular application inside a large legacy build.&lt;/p&gt;

&lt;p&gt;Our Angular 19 code currently lives in an environment where &lt;strong&gt;TypeScript 4.9&lt;/strong&gt; is still part of the build constraints because of the existing webpack setup and the old application's dependencies.&lt;/p&gt;

&lt;p&gt;So we're effectively putting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Angular 19
      +
TypeScript 4.9
      +
AngularJS 1.8.3
      +
legacy webpack configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;into one application.&lt;/p&gt;

&lt;p&gt;It works.&lt;/p&gt;

&lt;p&gt;But it makes you appreciate how many layers exist between a framework and the code you actually write.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Interactive Map: Where the Two Worlds Meet
&lt;/h1&gt;

&lt;p&gt;The office map is currently one of the largest remaining migration challenges.&lt;/p&gt;

&lt;p&gt;We are rebuilding the administrative map editor using Angular and Fabric.js.&lt;/p&gt;

&lt;p&gt;But the old employee map and some of the existing map infrastructure still belong to the legacy world.&lt;/p&gt;

&lt;p&gt;That means the new implementation needs to understand data produced by the old engine.&lt;/p&gt;

&lt;p&gt;One particularly painful example is coordinates and &lt;code&gt;viewConfig&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the new editor interprets the old coordinate system incorrectly, objects don't simply throw an error.&lt;/p&gt;

&lt;p&gt;They &lt;strong&gt;move&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The desks are there.&lt;/p&gt;

&lt;p&gt;The data is there.&lt;/p&gt;

&lt;p&gt;Fabric.js is working.&lt;/p&gt;

&lt;p&gt;But everything appears in the wrong place.&lt;/p&gt;

&lt;p&gt;This is a particularly good example of why framework migration isn't just about rewriting components.&lt;/p&gt;

&lt;p&gt;You are migrating &lt;strong&gt;assumptions&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why We Don't Migrate Everything at Once
&lt;/h1&gt;

&lt;p&gt;At this point, it might be tempting to say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We've already migrated half the application. Why not just finish it?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because the remaining half isn't necessarily the easy half.&lt;/p&gt;

&lt;p&gt;Some of the oldest parts are also the most interconnected.&lt;/p&gt;

&lt;p&gt;The office map, for example, touches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;legacy data models;&lt;/li&gt;
&lt;li&gt;coordinates;&lt;/li&gt;
&lt;li&gt;rendering;&lt;/li&gt;
&lt;li&gt;permissions;&lt;/li&gt;
&lt;li&gt;routing;&lt;/li&gt;
&lt;li&gt;administration;&lt;/li&gt;
&lt;li&gt;employee-facing functionality.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Moving it isn't equivalent to moving a simple settings page.&lt;/p&gt;

&lt;p&gt;The migration has to follow the actual dependency graph of the product, not an arbitrary percentage target.&lt;/p&gt;




&lt;h1&gt;
  
  
  Our Current Migration Strategy
&lt;/h1&gt;

&lt;p&gt;The strategy has become relatively simple.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. New features go to Angular
&lt;/h3&gt;

&lt;p&gt;We don't create new AngularJS functionality unless there is a very good reason.&lt;/p&gt;

&lt;p&gt;This prevents the legacy side from growing again.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Migrate complete feature boundaries
&lt;/h3&gt;

&lt;p&gt;Whenever possible, we move an entire user-facing area instead of converting random components one by one.&lt;/p&gt;

&lt;p&gt;This reduces the amount of hybrid code.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Keep bridges temporary
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;downgradeComponent&lt;/code&gt;, &lt;code&gt;downgradeInjectable&lt;/code&gt;, &lt;code&gt;$injector&lt;/code&gt;, and other compatibility mechanisms are tools for migration.&lt;/p&gt;

&lt;p&gt;They aren't the architecture we want to keep forever.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Don't migrate unrelated infrastructure
&lt;/h3&gt;

&lt;p&gt;If a legacy service still works and isn't blocking the current migration, we leave it alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Remove old code only after the new implementation is stable
&lt;/h3&gt;

&lt;p&gt;The goal isn't to have two implementations forever.&lt;/p&gt;

&lt;p&gt;The goal is to create a safe path from one implementation to another.&lt;/p&gt;




&lt;h1&gt;
  
  
  What We Would Do Differently
&lt;/h1&gt;

&lt;p&gt;If we were starting this migration today, I would pay more attention to the boundaries before writing the first Angular component.&lt;/p&gt;

&lt;p&gt;In particular:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;define ownership of shared state earlier;&lt;/li&gt;
&lt;li&gt;establish clearer rules for AngularJS/Angular service dependencies;&lt;/li&gt;
&lt;li&gt;document which side owns each piece of infrastructure;&lt;/li&gt;
&lt;li&gt;define a migration strategy for routing earlier;&lt;/li&gt;
&lt;li&gt;identify coordinate systems and data contracts before touching UI code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hardest problems weren't caused by Angular.&lt;/p&gt;

&lt;p&gt;They were caused by &lt;strong&gt;unclear boundaries between the old and new systems&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Most Important Lesson
&lt;/h1&gt;

&lt;p&gt;A framework migration is often described as a technical rewrite.&lt;/p&gt;

&lt;p&gt;In practice, it is closer to &lt;strong&gt;changing the architecture of a running system without stopping the system&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's a different problem.&lt;/p&gt;

&lt;p&gt;You don't get a clean slate.&lt;/p&gt;

&lt;p&gt;You have users.&lt;/p&gt;

&lt;p&gt;You have production data.&lt;/p&gt;

&lt;p&gt;You have business requirements.&lt;/p&gt;

&lt;p&gt;You have old code that still works.&lt;/p&gt;

&lt;p&gt;And you have new code that needs to coexist with it.&lt;/p&gt;

&lt;p&gt;The most useful mindset we found was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't try to make the old system disappear first. Make the new system capable of replacing it piece by piece.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once that becomes the goal, the migration becomes much more manageable.&lt;/p&gt;




&lt;h1&gt;
  
  
  Where We Are Now
&lt;/h1&gt;

&lt;p&gt;We are approximately halfway through the migration.&lt;/p&gt;

&lt;p&gt;Angular 19 is already responsible for a significant part of the production application.&lt;/p&gt;

&lt;p&gt;New functionality is being built on Angular.&lt;/p&gt;

&lt;p&gt;Legacy functionality is gradually being replaced.&lt;/p&gt;

&lt;p&gt;The AngularJS application is still responsible for the bootstrap process and routing, so we aren't at the finish line yet.&lt;/p&gt;

&lt;p&gt;But the important part is that the direction has changed.&lt;/p&gt;

&lt;p&gt;AngularJS is no longer where the product grows.&lt;/p&gt;

&lt;p&gt;Angular is.&lt;/p&gt;

&lt;p&gt;And that's probably the most meaningful milestone of the migration so far.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;A Big-Bang rewrite gives you the cleanest architecture on paper.&lt;/p&gt;

&lt;p&gt;A hybrid migration gives you something more valuable in a production product:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a way to improve the architecture without stopping the product.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It isn't always elegant.&lt;/p&gt;

&lt;p&gt;Sometimes you'll have two dependency injection systems.&lt;/p&gt;

&lt;p&gt;Two translation libraries.&lt;/p&gt;

&lt;p&gt;An old router controlling new components.&lt;/p&gt;

&lt;p&gt;A legacy service being injected into modern Angular code.&lt;/p&gt;

&lt;p&gt;And a TODO saying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TODO: remove after migration&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that survives much longer than anyone expected.&lt;/p&gt;

&lt;p&gt;But if each new feature moves the system a little further toward the target architecture, eventually the old framework becomes the smaller part of the application.&lt;/p&gt;

&lt;p&gt;And one day, you can finally remove it.&lt;/p&gt;

&lt;p&gt;We're not there yet.&lt;/p&gt;

&lt;p&gt;But we're getting closer.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What We Learned Rewriting an Interactive Map Editor: Fabric.js, CORS, and 20,000 Lines of Legacy TypeScript</title>
      <dc:creator>Aleksandr Gusev</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:43:01 +0000</pubDate>
      <link>https://dev.to/aleksandr_gusev_it/what-we-learned-rewriting-an-interactive-map-editor-fabricjs-cors-and-20000-lines-of-legacy-2njb</link>
      <guid>https://dev.to/aleksandr_gusev_it/what-we-learned-rewriting-an-interactive-map-editor-fabricjs-cors-and-20000-lines-of-legacy-2njb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A story about how migrating an interactive office map editor turned into an engineering investigation involving Fabric.js, &lt;code&gt;tainted canvas&lt;/code&gt;, and an architecture that's finally easy to extend.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In most software projects, one sentence usually makes every developer nervous:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Let's rewrite this module from scratch."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It often means months of development, regression risks, and endless architecture discussions.&lt;/p&gt;

&lt;p&gt;Our project was no different.&lt;/p&gt;

&lt;p&gt;We develop, a workspace management platform that allows companies to manage office spaces and book desks. One of its core features is an interactive office map editor, where administrators upload floor plans, place desks and meeting rooms, and publish maps for employees.&lt;/p&gt;

&lt;p&gt;Over the years, this editor slowly evolved into a real monolith.&lt;/p&gt;

&lt;p&gt;And the problem wasn't simply the number of lines of code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where It All Started
&lt;/h2&gt;

&lt;p&gt;The editor dated back to the AngularJS era.&lt;/p&gt;

&lt;p&gt;The main component had gradually grown into a single file responsible for almost everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;loading maps&lt;/li&gt;
&lt;li&gt;working with Fabric.js&lt;/li&gt;
&lt;li&gt;CRUD operations&lt;/li&gt;
&lt;li&gt;keyboard shortcuts&lt;/li&gt;
&lt;li&gt;dialogs&lt;/li&gt;
&lt;li&gt;saving&lt;/li&gt;
&lt;li&gt;event handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main editor component alone contained nearly &lt;strong&gt;2,270 lines of code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Behind it lived another codebase — the map engine itself.&lt;/p&gt;

&lt;p&gt;Almost &lt;strong&gt;20,000 lines of TypeScript&lt;/strong&gt; spread across more than 230 files.&lt;/p&gt;

&lt;p&gt;One of the biggest architectural issues was an infinite rendering loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;fabric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;util&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestAnimFrame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even when the user wasn't interacting with the editor, rendering continued forever.&lt;/p&gt;

&lt;p&gt;It worked.&lt;/p&gt;

&lt;p&gt;But every new feature became more expensive to build.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why We Decided to Rewrite It
&lt;/h2&gt;

&lt;p&gt;The motivation wasn't AngularJS itself.&lt;/p&gt;

&lt;p&gt;The real reason was business requirements.&lt;/p&gt;

&lt;p&gt;The product needed completely new capabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;map drafts&lt;/li&gt;
&lt;li&gt;safe publishing&lt;/li&gt;
&lt;li&gt;high-quality printing&lt;/li&gt;
&lt;li&gt;multiple workspace modes&lt;/li&gt;
&lt;li&gt;easier support for new object types&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every new feature pushed harder against the existing architecture.&lt;/p&gt;

&lt;p&gt;Eventually it became obvious:&lt;/p&gt;

&lt;p&gt;We weren't fighting individual bugs anymore.&lt;/p&gt;

&lt;p&gt;We were fighting the architecture itself.&lt;/p&gt;

&lt;p&gt;Instead of performing a risky Big Bang migration, we decided to build an entirely new administration module alongside the existing viewer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building a New Architecture
&lt;/h2&gt;

&lt;p&gt;We kept Fabric.js.&lt;/p&gt;

&lt;p&gt;Everything around it changed.&lt;/p&gt;

&lt;p&gt;Instead of one massive controller, we introduced clear responsibilities.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Map Editor
        │
        ▼
   Map Engine
        │
 ┌──────┼─────────┐
 │      │         │
 ▼      ▼         ▼
Viewport Registry Changes
                Tracker
        │
        ▼
     Fabric.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each subsystem became responsible for exactly one concern.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Viewport&lt;/strong&gt; controls camera movement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Object Registry&lt;/strong&gt; manages map entities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changes Tracker&lt;/strong&gt; knows only about modifications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map Engine&lt;/strong&gt; exposes a single API for the editor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most pleasant surprise came later.&lt;/p&gt;

&lt;p&gt;Adding new functionality no longer required touching half of the application.&lt;/p&gt;




&lt;h2&gt;
  
  
  Moving the Camera Instead of Moving Objects
&lt;/h2&gt;

&lt;p&gt;One architectural decision simplified much more than we expected.&lt;/p&gt;

&lt;p&gt;Instead of physically moving every desk on the canvas, we move the camera using Fabric's &lt;code&gt;viewportTransform&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Objects always stay in their original coordinates.&lt;/p&gt;

&lt;p&gt;This dramatically simplified:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;zooming&lt;/li&gt;
&lt;li&gt;panning&lt;/li&gt;
&lt;li&gt;exporting&lt;/li&gt;
&lt;li&gt;printing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes one architectural decision solves several future problems at once.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Hardest Problem Wasn't Angular
&lt;/h1&gt;

&lt;p&gt;When the migration started, we believed rewriting nearly 20,000 lines of TypeScript would be the hardest challenge.&lt;/p&gt;

&lt;p&gt;It wasn't.&lt;/p&gt;

&lt;p&gt;The most difficult part looked completely harmless.&lt;/p&gt;

&lt;p&gt;We simply needed to print the office map.&lt;/p&gt;




&lt;h2&gt;
  
  
  Attempt #1
&lt;/h2&gt;

&lt;p&gt;The obvious solution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, browser printing renders the DOM at roughly screen resolution.&lt;/p&gt;

&lt;p&gt;For large office maps, the result was blurry.&lt;/p&gt;

&lt;p&gt;Even worse, parts of the editor UI appeared in the printed document.&lt;/p&gt;

&lt;p&gt;Not acceptable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Attempt #2
&lt;/h2&gt;

&lt;p&gt;The next idea looked much better.&lt;/p&gt;

&lt;p&gt;Export the Fabric canvas as a high-resolution image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataURL&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, the browser answered with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SecurityError:
The canvas has been tainted by cross-origin data.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;The floor plan image was stored on Amazon S3.&lt;/p&gt;

&lt;p&gt;Although it rendered perfectly inside the browser, exporting the canvas became impossible because the background image wasn't available with the required CORS configuration.&lt;/p&gt;

&lt;p&gt;The browser was doing exactly what it was supposed to do.&lt;/p&gt;




&lt;h2&gt;
  
  
  Attempt #3
&lt;/h2&gt;

&lt;p&gt;This one finally worked.&lt;/p&gt;

&lt;p&gt;Instead of exporting a single PNG, we split printing into two independent layers.&lt;/p&gt;

&lt;p&gt;The background image remained untouched.&lt;/p&gt;

&lt;p&gt;The Fabric objects were exported separately without the background.&lt;/p&gt;

&lt;p&gt;Conceptually, the pipeline became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Background Image
        +
Objects Layer
        =
Printable Document
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation looked roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;objectsDataUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;exportCanvasRegion&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;hideBackground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;backgroundUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;objectsDataUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The solution looked more complicated.&lt;/p&gt;

&lt;p&gt;In reality, it proved to be much more reliable.&lt;/p&gt;




&lt;h2&gt;
  
  
  One More Unexpected Problem
&lt;/h2&gt;

&lt;p&gt;After solving the &lt;code&gt;SecurityError&lt;/code&gt;, another issue appeared.&lt;/p&gt;

&lt;p&gt;Objects slowly drifted away from their correct positions during printing.&lt;/p&gt;

&lt;p&gt;The reason turned out to be surprisingly subtle.&lt;/p&gt;

&lt;p&gt;The background image was positioned in pixels.&lt;/p&gt;

&lt;p&gt;The overlay objects were positioned using percentages.&lt;/p&gt;

&lt;p&gt;When the browser recalculated layout for printing, the two layers no longer aligned.&lt;/p&gt;

&lt;p&gt;The final fix was surprisingly simple.&lt;/p&gt;

&lt;p&gt;We switched background positioning to percentage-based coordinates as well.&lt;/p&gt;

&lt;p&gt;Immediately both layers aligned perfectly again.&lt;/p&gt;

&lt;p&gt;Problems like this are often the hardest to debug.&lt;/p&gt;

&lt;p&gt;Not because the code is wrong.&lt;/p&gt;

&lt;p&gt;But because the coordinate model itself is wrong.&lt;/p&gt;




&lt;h1&gt;
  
  
  What Changed After the Migration
&lt;/h1&gt;

&lt;p&gt;Interestingly, success wasn't measured by fewer lines of code.&lt;/p&gt;

&lt;p&gt;The new implementation actually introduced new functionality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;draft support&lt;/li&gt;
&lt;li&gt;printing&lt;/li&gt;
&lt;li&gt;safe publishing&lt;/li&gt;
&lt;li&gt;multiple workspace modes&lt;/li&gt;
&lt;li&gt;a much cleaner architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The number of lines became a meaningless metric.&lt;/p&gt;

&lt;p&gt;The important improvement was something else.&lt;/p&gt;

&lt;p&gt;Adding new features no longer felt dangerous.&lt;/p&gt;




&lt;h1&gt;
  
  
  What We Learned
&lt;/h1&gt;

&lt;p&gt;Projects like this remind you that the hardest engineering challenges rarely belong to a particular framework.&lt;/p&gt;

&lt;p&gt;Angular evolves.&lt;/p&gt;

&lt;p&gt;Fabric.js evolves.&lt;/p&gt;

&lt;p&gt;TypeScript evolves.&lt;/p&gt;

&lt;p&gt;But the real complexity usually appears somewhere between these technologies.&lt;/p&gt;

&lt;p&gt;Sometimes rewriting twenty thousand lines of code isn't the hardest part of the project.&lt;/p&gt;

&lt;p&gt;Sometimes it's the seemingly innocent &lt;strong&gt;Print&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;That single feature forced us to better understand browser rendering, CORS, Canvas security, coordinate systems, and software architecture than any framework migration ever could.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Looking back, the migration wasn't really about Angular.&lt;/p&gt;

&lt;p&gt;It was about making the product easier to evolve.&lt;/p&gt;

&lt;p&gt;The new architecture didn't dramatically reduce the amount of code.&lt;/p&gt;

&lt;p&gt;Instead, it reduced the cost of future changes.&lt;/p&gt;

&lt;p&gt;Sometimes that's a far more valuable metric than lines of code.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;If this story is interesting, I'd be happy to write a follow-up article explaining how we designed our &lt;code&gt;MapEngine&lt;/code&gt;, why we decided to keep Fabric.js, and how we migrated the administration module without stopping product development.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is based on our experience building and maintaining an enterprise workspace management platform. Code samples have been simplified, and implementation details have been generalized to respect product confidentiality while preserving the engineering decisions and lessons learned.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  angular #typescript #webdev #architecture
&lt;/h1&gt;

</description>
      <category>angular</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Should Angular Apps Still Rely on RxJS in 2026?</title>
      <dc:creator>Aleksandr Gusev</dc:creator>
      <pubDate>Fri, 22 May 2026 17:09:09 +0000</pubDate>
      <link>https://dev.to/aleksandr_gusev_it/should-angular-apps-still-rely-on-rxjs-in-2025-92p</link>
      <guid>https://dev.to/aleksandr_gusev_it/should-angular-apps-still-rely-on-rxjs-in-2025-92p</guid>
      <description>&lt;p&gt;Angular is going through a quiet but important shift in how reactivity is handled. With the introduction of Signals, many developers are starting to ask a simple question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do we still need RxJS in Angular applications in 2026?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The short answer is: yes — but not everywhere.&lt;/p&gt;

&lt;p&gt;The real answer is more interesting, and it’s about architecture, not preference.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two different mental models
&lt;/h2&gt;

&lt;p&gt;To understand the discussion, it helps to separate two fundamentally different models of reactivity.&lt;/p&gt;

&lt;h3&gt;
  
  
  RxJS: asynchronous streams
&lt;/h3&gt;

&lt;p&gt;RxJS is built around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;event streams&lt;/li&gt;
&lt;li&gt;asynchronous composition&lt;/li&gt;
&lt;li&gt;time-based operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It shines when dealing with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP requests&lt;/li&gt;
&lt;li&gt;WebSockets&lt;/li&gt;
&lt;li&gt;complex async workflows&lt;/li&gt;
&lt;li&gt;event orchestration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RxJS is about &lt;strong&gt;time and events&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Signals: local reactive state
&lt;/h3&gt;

&lt;p&gt;Signals represent a different idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;synchronous reactivity&lt;/li&gt;
&lt;li&gt;local state tracking&lt;/li&gt;
&lt;li&gt;explicit dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are best suited for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UI state&lt;/li&gt;
&lt;li&gt;derived state&lt;/li&gt;
&lt;li&gt;component-level reactivity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Signals are about &lt;strong&gt;state and UI consistency&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Code comparison
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Signals example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;computed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doubled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Signals make state and dependencies explicit. No subscriptions, no hidden wiring.&lt;/p&gt;




&lt;h3&gt;
  
  
  RxJS example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BehaviorSubject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BehaviorSubject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doubled$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;count$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RxJS introduces a stream-based model where everything flows through observables.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real problem is not technology, but mixing models
&lt;/h2&gt;

&lt;p&gt;The biggest architectural issue in modern Angular applications is not choosing between RxJS and Signals.&lt;/p&gt;

&lt;p&gt;It is &lt;strong&gt;mixing them without clear boundaries&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When both are used everywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;state becomes duplicated&lt;/li&gt;
&lt;li&gt;logic becomes scattered&lt;/li&gt;
&lt;li&gt;it becomes unclear where the “source of truth” lives&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where RxJS is still the right tool
&lt;/h2&gt;

&lt;p&gt;Even in 2026, RxJS is still essential:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP request pipelines&lt;/li&gt;
&lt;li&gt;cancellation and retry logic&lt;/li&gt;
&lt;li&gt;real-time data streams&lt;/li&gt;
&lt;li&gt;coordination of multiple async sources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the problem involves &lt;strong&gt;time, concurrency, or event composition&lt;/strong&gt;, RxJS is still the better abstraction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Signals are a better fit
&lt;/h2&gt;

&lt;p&gt;Signals work better when the problem is local and state-driven:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;component state&lt;/li&gt;
&lt;li&gt;derived UI values&lt;/li&gt;
&lt;li&gt;form state&lt;/li&gt;
&lt;li&gt;simple reactive bindings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Signals reduce unnecessary complexity and make data flow more explicit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architectural boundary
&lt;/h2&gt;

&lt;p&gt;A simple rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;RxJS handles the outside world.&lt;br&gt;
Signals handle the UI world.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;RxJS = integration layer&lt;/li&gt;
&lt;li&gt;Signals = presentation layer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When this boundary is respected, Angular applications become significantly easier to maintain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;In 2026, RxJS is not obsolete in Angular — it is simply no longer the only reactive model.&lt;/p&gt;

&lt;p&gt;Signals introduce a second layer that allows developers to simplify UI logic without abandoning reactive power.&lt;/p&gt;

&lt;p&gt;The best Angular applications are not those that choose one over the other, but those that clearly define when to use each.&lt;/p&gt;

&lt;p&gt;Architecture is not about tools.&lt;br&gt;
It is about boundaries.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>angular</category>
      <category>signals</category>
      <category>rxjs</category>
    </item>
    <item>
      <title>Angular Signals</title>
      <dc:creator>Aleksandr Gusev</dc:creator>
      <pubDate>Fri, 24 Apr 2026 18:34:08 +0000</pubDate>
      <link>https://dev.to/aleksandr_gusev_it/angular-signals-2bd2</link>
      <guid>https://dev.to/aleksandr_gusev_it/angular-signals-2bd2</guid>
      <description>&lt;h1&gt;
  
  
  Angular Signals — A Shift in Angular Reactivity
&lt;/h1&gt;

&lt;p&gt;Angular has changed significantly over the past few years. What was once often perceived as a heavy framework with complex change detection and a strong reliance on Zone.js is gradually moving toward a more explicit and predictable reactivity model.&lt;/p&gt;

&lt;p&gt;One of the key steps in this direction is &lt;strong&gt;Signals&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are Signals?
&lt;/h2&gt;

&lt;p&gt;Signals in Angular introduce a new way of handling state that makes reactivity more explicit.&lt;/p&gt;

&lt;p&gt;In simple terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Angular used to rely heavily on Zone.js and implicit change detection&lt;/li&gt;
&lt;li&gt;Now it introduces a model where state and dependencies are explicitly defined&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Signal is a reactive primitive that holds a value and notifies the system when that value changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;One of the long-standing challenges in Angular was not complexity itself, but &lt;strong&gt;implicit reactivity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Developers often had to deal with questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What exactly triggered a UI update?&lt;/li&gt;
&lt;li&gt;Why did this component re-render?&lt;/li&gt;
&lt;li&gt;Where does change detection actually stop?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Signals make this behavior more explicit and predictable.&lt;/p&gt;

&lt;p&gt;Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dependencies are directly readable&lt;/li&gt;
&lt;li&gt;updates are more granular&lt;/li&gt;
&lt;li&gt;less framework-level “magic” is involved&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A simple mindset shift
&lt;/h2&gt;

&lt;p&gt;Before Signals:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Angular will figure out what needs to update.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With Signals:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I explicitly define what depends on what.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is not just a technical change — it’s a conceptual shift in how we think about UI reactivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes in practice
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. More predictable state management
&lt;/h3&gt;

&lt;p&gt;Signals move Angular closer to simple reactive primitives instead of relying on global change detection mechanisms.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Reduced reliance on Zone.js
&lt;/h3&gt;

&lt;p&gt;Angular is gradually moving toward a model where Zone.js is no longer the central piece of reactivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Note: What is Zone.js?
&lt;/h2&gt;

&lt;p&gt;Zone.js is a library that was historically at the core of Angular’s change detection system.&lt;/p&gt;

&lt;p&gt;In simple terms, it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tracks asynchronous operations (setTimeout, HTTP requests, events)&lt;/li&gt;
&lt;li&gt;and notifies Angular when it should check for UI updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allowed Angular to automatically update the UI without explicit developer intervention.&lt;/p&gt;

&lt;p&gt;However, this convenience comes with a trade-off: behavior becomes less transparent, since it is not always clear what triggered a re-render.&lt;/p&gt;

&lt;p&gt;Signals address part of this by making reactivity more explicit and controlled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signals vs RxJS
&lt;/h2&gt;

&lt;p&gt;Signals do not replace RxJS — they serve different purposes.&lt;/p&gt;

&lt;p&gt;RxJS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;streams and events&lt;/li&gt;
&lt;li&gt;asynchronous workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local state&lt;/li&gt;
&lt;li&gt;synchronous reactivity&lt;/li&gt;
&lt;li&gt;UI-driven updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They complement each other rather than compete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Signals shine
&lt;/h2&gt;

&lt;p&gt;Signals are especially useful in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;complex UI state&lt;/li&gt;
&lt;li&gt;forms and interactive interfaces&lt;/li&gt;
&lt;li&gt;applications where predictability matters&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Signals are not just a new API in Angular.&lt;/p&gt;

&lt;p&gt;They represent a shift toward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more explicit reactivity&lt;/li&gt;
&lt;li&gt;more predictable UI behavior&lt;/li&gt;
&lt;li&gt;a simpler mental model for developers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s one of the most meaningful evolutions in Angular in recent years — aimed at reducing hidden complexity without sacrificing power.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>angular</category>
      <category>signals</category>
    </item>
    <item>
      <title>Why Being a Senior Developer Is Less About Tech Stack and More About Thinking</title>
      <dc:creator>Aleksandr Gusev</dc:creator>
      <pubDate>Tue, 20 Jan 2026 18:24:41 +0000</pubDate>
      <link>https://dev.to/aleksandr_gusev_it/why-being-a-senior-developer-is-less-about-tech-stack-and-more-about-thinking-267l</link>
      <guid>https://dev.to/aleksandr_gusev_it/why-being-a-senior-developer-is-less-about-tech-stack-and-more-about-thinking-267l</guid>
      <description>&lt;p&gt;After nearly a decade in professional web development, I’ve noticed that many discussions about seniority focus on the wrong things. People ask which tech stack you need to learn or how many years it takes to become a senior. In my experience, those questions miss the point.&lt;/p&gt;

&lt;p&gt;Being a senior developer is not about specific technologies or job titles. It’s about the way you think and the responsibility you take.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Tech Stack Is a Tool, Not a Level
&lt;/h2&gt;

&lt;p&gt;Throughout my career, I’ve worked with different frameworks and approaches. Some were trendy, others were already considered outdated when they were introduced. Almost all of them eventually changed.&lt;/p&gt;

&lt;p&gt;What didn’t change was the level of responsibility.&lt;/p&gt;

&lt;p&gt;A junior developer worries about &lt;em&gt;how to write code&lt;/em&gt;.&lt;br&gt;
A mid-level developer worries about &lt;em&gt;how to write correct code&lt;/em&gt;.&lt;br&gt;
A senior developer worries about &lt;em&gt;why this code should exist at all&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Responsibility Comes Before the Title
&lt;/h2&gt;

&lt;p&gt;Senior-level thinking often appears long before the official title:&lt;br&gt;
— when you think about maintainability, not just delivery;&lt;br&gt;
— when you question requirements instead of blindly implementing them;&lt;br&gt;
— when you understand that there is no perfect solution, only trade-offs.&lt;/p&gt;

&lt;p&gt;At some point, you stop writing code for machines and start writing it for people who will read and maintain it months or years later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Is Not About Diagrams
&lt;/h2&gt;

&lt;p&gt;Architecture is often associated with clean diagrams and well-known patterns. In reality, architecture is more about:&lt;br&gt;
— where the system can break;&lt;br&gt;
— how expensive changes will be;&lt;br&gt;
— who will maintain it and how.&lt;/p&gt;

&lt;p&gt;Sometimes the best architectural decision is the most boring one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experience Is Not the Number of Projects
&lt;/h2&gt;

&lt;p&gt;Experience doesn’t come from the number of projects or lines of code. It comes from situations where you:&lt;br&gt;
— had to fix someone else’s mistakes;&lt;br&gt;
— rolled back your own wrong decisions;&lt;br&gt;
— supported a product for years instead of shipping it and moving on.&lt;/p&gt;

&lt;p&gt;These moments are what actually shape professional judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Being a senior developer is not a goal or a status. It’s a side effect of long-term work on real products and taking responsibility for the consequences of your decisions.&lt;/p&gt;

&lt;p&gt;You can learn a tech stack.&lt;br&gt;
Professional thinking takes time to earn.&lt;/p&gt;

&lt;p&gt;This way of thinking has helped me make better long-term decisions in real-world products.&lt;/p&gt;

</description>
      <category>career</category>
      <category>discuss</category>
      <category>softwaredevelopment</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
