<?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: Mohammad</title>
    <description>The latest articles on DEV Community by Mohammad (@calligra).</description>
    <link>https://dev.to/calligra</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%2F1537554%2Fb2c296b3-3398-4ad8-9892-752bdb69602a.png</url>
      <title>DEV Community: Mohammad</title>
      <link>https://dev.to/calligra</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/calligra"/>
    <language>en</language>
    <item>
      <title>OverlayFS Explained: How Docker's Filesystem Layers Actually Work</title>
      <dc:creator>Mohammad</dc:creator>
      <pubDate>Sun, 28 Jun 2026 20:15:33 +0000</pubDate>
      <link>https://dev.to/calligra/overlayfs-explained-how-dockers-filesystem-layers-actually-work-1ja2</link>
      <guid>https://dev.to/calligra/overlayfs-explained-how-dockers-filesystem-layers-actually-work-1ja2</guid>
      <description>&lt;h1&gt;
  
  
  Preface
&lt;/h1&gt;

&lt;p&gt;I have planned to write a series of articles to understand Docker under the hood and containers. I will start with the Overlay filesystem 🙂, but every journey should begin from the fundamentals.&lt;/p&gt;

&lt;p&gt;When you hear “Docker”, the first thing that comes to mind is “container”.&lt;/p&gt;

&lt;p&gt;And what about “Kubernetes”? Again, containers.&lt;/p&gt;

&lt;p&gt;Two different systems, same mental model.&lt;/p&gt;

&lt;p&gt;So what exactly are containers? And are they truly isolated and independent units of software?&lt;/p&gt;

&lt;p&gt;The short answer is: &lt;strong&gt;yes, but not in the way people usually think.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Containers have existed in Unix/Linux systems long before Docker. What Docker and Kubernetes did was to make them &lt;strong&gt;portable, standardized, and operationally usable at scale&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Recreation Time
&lt;/h3&gt;

&lt;p&gt;Sometimes I say &lt;strong&gt;K8s&lt;/strong&gt; because there are 8 letters between “K” and “s”.&lt;/p&gt;

&lt;p&gt;K[ubernete]s → “ubernete” = 8 characters → K8s.&lt;/p&gt;

&lt;p&gt;Just a naming shortcut. Nothing deeper.&lt;/p&gt;




&lt;h3&gt;
  
  
  Fun practice
&lt;/h3&gt;

&lt;p&gt;How would you shorten my family name “Ghalambaz”?&lt;/p&gt;

&lt;p&gt;Maybe “G7z”. Now try your own version.&lt;/p&gt;




&lt;p&gt;Let’s get back to containers.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is a container really?
&lt;/h2&gt;

&lt;p&gt;A container is &lt;strong&gt;not a virtual machine&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead, it is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A normal Linux process that is isolated using kernel features like namespaces and cgroups.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This isolation makes the process &lt;em&gt;feel&lt;/em&gt; like it has its own system, but in reality it is still running on the host kernel.&lt;/p&gt;

&lt;p&gt;One of the core mechanisms behind this isolation is Linux’s namespace system, which includes a system call called &lt;code&gt;unshare&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So yes, &lt;code&gt;unshare&lt;/code&gt; is powerful, but it is only one piece of a much larger puzzle.&lt;/p&gt;

&lt;p&gt;We will come back to this later.&lt;/p&gt;

&lt;p&gt;For now, we start with something even more fundamental to containers:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;the filesystem layer model used by Docker: OverlayFS&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  File Systems
&lt;/h1&gt;

&lt;p&gt;Have you ever heard of filesystems like FAT32, NTFS, or EXT4?&lt;/p&gt;

&lt;p&gt;They define how the operating system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stores data on disk&lt;/li&gt;
&lt;li&gt;organizes files&lt;/li&gt;
&lt;li&gt;manages metadata&lt;/li&gt;
&lt;li&gt;retrieves data efficiently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each filesystem has different design goals, but I are not focusing on them here.&lt;/p&gt;

&lt;p&gt;Instead, I focus on a special filesystem type:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;OverlayFS is the foundation of Docker image layering&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  What is OverlayFS?
&lt;/h1&gt;

&lt;p&gt;OverlayFS is a &lt;strong&gt;union filesystem&lt;/strong&gt; in Linux.&lt;/p&gt;

&lt;p&gt;It allows multiple directories (called layers) to appear as a single unified directory.&lt;/p&gt;




&lt;h2&gt;
  
  
  Simple mental model
&lt;/h2&gt;

&lt;p&gt;Imagine two directories:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Directory A&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A.txt&lt;/li&gt;
&lt;li&gt;C.txt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Directory B&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;B.txt&lt;/li&gt;
&lt;li&gt;C.txt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now I “merge” them into a virtual directory &lt;strong&gt;M&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What do you see in M?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A.txt&lt;/li&gt;
&lt;li&gt;B.txt&lt;/li&gt;
&lt;li&gt;C.txt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But here is the important detail:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There is no real merging or copying happening.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;OverlayFS just &lt;em&gt;simulates&lt;/em&gt; a merged view.&lt;/p&gt;




&lt;h2&gt;
  
  
  Important questions (this is where it gets interesting)
&lt;/h2&gt;

&lt;p&gt;What happens when:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Two files have the same name?&lt;/li&gt;
&lt;li&gt;You modify a file from the merged view?&lt;/li&gt;
&lt;li&gt;Where does a new file actually get written?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are the exact problems OverlayFS solves using a layered model. Keep these three questions in mind. I will answer all of them in the next section.&lt;/p&gt;




&lt;h1&gt;
  
  
  OverlayFS Architecture
&lt;/h1&gt;

&lt;p&gt;OverlayFS has &lt;strong&gt;three main concepts&lt;/strong&gt;:&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Lower Layers (Read-only)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Multiple directories can exist&lt;/li&gt;
&lt;li&gt;They are stacked together&lt;/li&gt;
&lt;li&gt;They are immutable (read-only)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These layers typically represent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker image layers&lt;/li&gt;
&lt;li&gt;Base OS filesystem&lt;/li&gt;
&lt;li&gt;Application dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Answers question 1:&lt;/strong&gt; if a file exists in more than one lower layer, the layer listed first (the “topmost” one) wins. I will see this live in the hands-on section.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Upper Layer (Writable)
&lt;/h2&gt;

&lt;p&gt;This is where changes happen.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New files are created here&lt;/li&gt;
&lt;li&gt;Modified files are copied here (copy-on-write)&lt;/li&gt;
&lt;li&gt;It is writable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Answers question 3:&lt;/strong&gt; any new file you create always goes here, never into a lower layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Merged Layer (View Layer)
&lt;/h2&gt;

&lt;p&gt;This is what the user sees.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A unified view of all layers&lt;/li&gt;
&lt;li&gt;Read/write interface&lt;/li&gt;
&lt;li&gt;Does not contain real merged data&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  A note on &lt;code&gt;workdir&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;There is actually a small 4th directory the kernel needs, called &lt;code&gt;workdir&lt;/code&gt;. You will see it in the hands-on example below.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;workdir&lt;/code&gt; is not a “layer” you read from it’s internal scratch space the kernel uses to do copy-up operations safely (so a crash mid-write can’t corrupt your data). Two rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it must live on the &lt;strong&gt;same filesystem&lt;/strong&gt; as &lt;code&gt;upperdir&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;you should never read or write to it yourself. just create an empty folder and let the kernel manage it
&amp;gt; ⚠️ &lt;strong&gt;Don’t mix this up with Docker’s&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;WORKDIR&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;instruction.&lt;/strong&gt; They share a name, but they are completely different things:
&amp;gt; - OverlayFS &lt;code&gt;workdir&lt;/code&gt; : a kernel-level scratch directory for copy-up operations. Nothing to do with Docker directly; you only see it when mounting OverlayFS yourself or when reading &lt;code&gt;docker inspect&lt;/code&gt; output.
&amp;gt; - Docker &lt;code&gt;WORKDIR&lt;/code&gt; : a Dockerfile instruction that sets the &lt;em&gt;current working directory&lt;/em&gt; inside the image, for any &lt;code&gt;RUN&lt;/code&gt;, &lt;code&gt;CMD&lt;/code&gt;, &lt;code&gt;ENTRYPOINT&lt;/code&gt;, &lt;code&gt;COPY&lt;/code&gt;, and &lt;code&gt;ADD&lt;/code&gt; instructions that follow it. For example:
&amp;gt;
&amp;gt;

&lt;code&gt;plaintext
&amp;gt; WORKDIR /app
&amp;gt; COPY . .
&amp;gt; RUN npm install
&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Here, &lt;code&gt;COPY&lt;/code&gt; and &lt;code&gt;RUN&lt;/code&gt; both execute inside &lt;code&gt;/app&lt;/code&gt;. It’s a metadata-only instruction (no real file content), so as I covered earlier, it creates an empty layer.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Mental Model (Important)
&lt;/h1&gt;

&lt;p&gt;Think of it 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;User View (Merged)
                 |
     -----------------------------
     |            |             |
 Lower Layer   Lower Layer   Upper Layer
 (read-only)   (read-only)   (read-write)
                                  |
                              workdir
                          (kernel scratch space)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How file resolution works
&lt;/h2&gt;

&lt;p&gt;When you access a file:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Check Upper Layer
&lt;/h3&gt;

&lt;p&gt;If file exists → return it&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Check Lower Layers (top to bottom)
&lt;/h3&gt;

&lt;p&gt;If found → return it&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Not found → file does not exist
&lt;/h3&gt;




&lt;h2&gt;
  
  
  What happens on modification?
&lt;/h2&gt;

&lt;p&gt;This is where OverlayFS becomes powerful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario: file exists in lower layer
&lt;/h3&gt;

&lt;p&gt;If you modify it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;File is copied to upper layer&lt;/li&gt;
&lt;li&gt;Modification happens on the copied file&lt;/li&gt;
&lt;li&gt;Original lower layer remains unchanged&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is called:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Copy-on-write (CoW)&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Answers question 2:&lt;/strong&gt; modifying a file you see in the merged view never touches the lower layer. It only touches the copy in the upper layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  What happens when you create a new file?
&lt;/h2&gt;

&lt;p&gt;New files always go to the &lt;strong&gt;upper writable layer&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why OverlayFS matters for Docker
&lt;/h1&gt;

&lt;p&gt;Docker images are built using &lt;strong&gt;layers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each instruction in a Dockerfile adds an entry to the image, but not every instruction writes new files to disk. There are two kinds:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instructions that create a layer with real file content:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;RUN&lt;/code&gt; – runs a command and saves the file changes it makes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;COPY&lt;/code&gt; – copies files from your build context into the image&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ADD&lt;/code&gt; – like &lt;code&gt;COPY&lt;/code&gt;, but can also extract &lt;code&gt;.tar&lt;/code&gt; archives and fetch from a URL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Instructions that only add metadata (empty layer, 0 bytes):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ENV&lt;/code&gt; – sets an environment variable&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LABEL&lt;/code&gt; – adds metadata, like a tag&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CMD&lt;/code&gt; – sets the default command for the container&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ENTRYPOINT&lt;/code&gt; – sets the command that always runs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EXPOSE&lt;/code&gt; – documents which port the container listens on&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WORKDIR&lt;/code&gt; – sets the working directory&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;USER&lt;/code&gt; – sets which user runs the next instructions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ARG&lt;/code&gt; – defines a build-time variable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So yes, every instruction shows up in the image’s history. But only &lt;code&gt;RUN&lt;/code&gt;, &lt;code&gt;COPY&lt;/code&gt;, and &lt;code&gt;ADD&lt;/code&gt; actually change the filesystem and create a layer with real content. The rest just attach information to the image.&lt;/p&gt;

&lt;p&gt;You can check this yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker history &amp;lt;image-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Layers that come from metadata-only instructions will show &lt;code&gt;0B&lt;/code&gt; as their size.&lt;/p&gt;

&lt;p&gt;Example Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM ubuntu
RUN apt-get update
RUN apt-get install nginx
COPY app /app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;FROM&lt;/code&gt; brings in the base image layers, and each &lt;code&gt;RUN&lt;/code&gt; and the &lt;code&gt;COPY&lt;/code&gt; create one new content layer, then four content layers in total.&lt;/p&gt;

&lt;p&gt;When you run a container:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker stacks all image layers (lower layers)&lt;/li&gt;
&lt;li&gt;Adds a writable upper layer&lt;/li&gt;
&lt;li&gt;Uses OverlayFS to merge them&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  WSL connection
&lt;/h1&gt;

&lt;p&gt;WSL (especially WSL2) provides a &lt;strong&gt;real Linux kernel environment&lt;/strong&gt; on Windows.&lt;/p&gt;

&lt;p&gt;This enables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linux namespaces&lt;/li&gt;
&lt;li&gt;cgroups&lt;/li&gt;
&lt;li&gt;OverlayFS support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So OverlayFS is part of the Linux kernel features used inside WSL. This is also why Docker Desktop’s WSL2 backend can run real Linux containers on Windows without a separate full virtual machine.&lt;/p&gt;




&lt;h1&gt;
  
  
  In Practice (hands-on idea)
&lt;/h1&gt;

&lt;p&gt;Let’s create a simple structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir ofs
cd ofs

mkdir lower1 lower2 upper work merged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;work&lt;/code&gt; directory, this is the &lt;code&gt;workdir&lt;/code&gt; I talked about above. It’s easy to forget, but the mount will fail without it.&lt;/p&gt;

&lt;p&gt;Create files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;echo "A" &amp;gt; lower1/A.txt
echo "C1" &amp;gt; lower1/C.txt

echo "B" &amp;gt; lower2/B.txt
echo "C2" &amp;gt; lower2/C.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo mount -t overlay overlay \
  -o lowerdir=lower1:lower2,upperdir=upper,workdir=work \
  merged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ls merged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A.txt
B.txt
C.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what just happened, mapped to our actual files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lower1 (top priority)     lower2 (bottom)         upper (empty so far)
├── A.txt                 ├── B.txt
└── C.txt  ─┐             └── C.txt  ─┐           (nothing written yet)
            │  shadowed,  ┘            │
            │  lower1 wins
            ▼
      ┌─────────────────────────────────────┐
      │              merged                  │
      │  A.txt   ← from lower1               │
      │  B.txt   ← from lower2               │
      │  C.txt   ← from lower1 (wins,        │
      │            because it's listed first) │
      └─────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lower2/C.txt&lt;/code&gt; still exists on disk. it’s just hidden behind &lt;code&gt;lower1/C.txt&lt;/code&gt; in the merged view, because &lt;code&gt;lower1&lt;/code&gt; was listed first in &lt;code&gt;lowerdir=lower1:lower2&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test copy-on-write
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;echo "modified" &amp;gt; merged/A.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;upper/A.txt&lt;/code&gt; → exists (copied + modified)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lower1/A.txt&lt;/code&gt; → unchanged&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Docker connection
&lt;/h1&gt;

&lt;p&gt;You can inspect this same behavior in Docker, with a real image.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: pull and run an image
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker pull nginx:latest
docker run -d --name overlay-demo nginx:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: inspect the GraphDriver
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker inspect overlay-demo --format '{{json .GraphDriver}}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will get something like this  (your IDs will be different from these samples):&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;"Data"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"LowerDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/var/lib/docker/overlay2/abc123-init/diff:/var/lib/docker/overlay2/def456/diff"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"MergedDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/var/lib/docker/overlay2/abc123/merged"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"UpperDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/var/lib/docker/overlay2/abc123/diff"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"WorkDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/var/lib/docker/overlay2/abc123/work"&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;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"overlay2"&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;Notice the pattern matches exactly what I just did by hand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;LowerDir&lt;/code&gt; has multiple paths joined with &lt;code&gt;:&lt;/code&gt; , one per image layer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UpperDir&lt;/code&gt; is the container’s own writable layer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MergedDir&lt;/code&gt; is what the container actually sees as its root filesystem&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WorkDir&lt;/code&gt; is the kernel scratch space I mentioned earlier&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: open the directories and compare
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo ls /var/lib/docker/overlay2/&amp;lt;lower-id&amp;gt;/diff
sudo ls /var/lib/docker/overlay2/&amp;lt;upper-id&amp;gt;/diff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Right after &lt;code&gt;docker run&lt;/code&gt;, &lt;code&gt;UpperDir&lt;/code&gt; will be almost empty the container hasn’t changed anything yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: trigger copy-on-write live
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker exec overlay-demo sh -c "echo hello &amp;gt; /usr/share/nginx/html/test.txt"
sudo cat /var/lib/docker/overlay2/&amp;lt;upper-id&amp;gt;/diff/usr/share/nginx/html/test.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new file appears inside &lt;code&gt;UpperDir&lt;/code&gt;, not in any lower layer exactly the same behavior you saw with &lt;code&gt;merged/A.txt&lt;/code&gt; in the manual example.&lt;/p&gt;




&lt;h1&gt;
  
  
  Key Takeaway
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;OverlayFS gives containers the illusion of a complete, writable filesystem while actually using immutable layers + a writable delta layer&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is one of the core reasons Docker is fast and efficient.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>filesystem</category>
      <category>containers</category>
      <category>overlayfs</category>
    </item>
    <item>
      <title>Amazon S3, Object Stores, and the Directories That Don't Exist</title>
      <dc:creator>Mohammad</dc:creator>
      <pubDate>Thu, 25 Jun 2026 17:54:27 +0000</pubDate>
      <link>https://dev.to/calligra/amazon-s3-object-stores-and-the-directories-that-dont-exist-4a38</link>
      <guid>https://dev.to/calligra/amazon-s3-object-stores-and-the-directories-that-dont-exist-4a38</guid>
      <description>&lt;h2&gt;
  
  
  S3 and Object Storage
&lt;/h2&gt;

&lt;p&gt;Most of us have used Amazon S3 at some point and may have wondered:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If AWS provides S3, why are we using Linode Object Storage or MinIO instead?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The reason is that Amazon S3 is both a service and a de facto API standard for object storage.&lt;/p&gt;

&lt;p&gt;AWS provides S3 as its managed object storage service, but other vendors such as Linode and MinIO also offer object storage solutions that implement the S3-compatible API. This allows applications written for S3 to work with different storage providers with little or no code changes.&lt;/p&gt;

&lt;p&gt;Recently, we migrated our object storage from MinIO to Linode. Although we do not use AWS S3 itself, our applications still communicate using the S3-compatible API.&lt;/p&gt;

&lt;p&gt;One important thing to keep in mind is that third-party implementations are not always identical to AWS S3. Some providers may only partially implement the S3 API, behave slightly differently, or not support all available features.&lt;/p&gt;




&lt;h2&gt;
  
  
  Directories in Object Storage
&lt;/h2&gt;

&lt;p&gt;When working with object storage, you've probably seen structures that look like directories:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;versions/
└── object/
    └── g0/
        └── something.xxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, this looks very similar to a traditional filesystem. But are these actually directories?&lt;/p&gt;

&lt;p&gt;The answer is &lt;strong&gt;no&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Object storage does not store files in real directories. Instead, every object is stored with a unique key (identifier). What looks like a path is simply part of the object's name.&lt;/p&gt;

&lt;p&gt;For example, an object may have the following key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;versions/object/g0/something.xxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a single object key, not a file stored inside nested directories.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/&lt;/code&gt; character is simply a naming convention used to organize object names. Management tools and UIs use these prefixes to simulate a directory structure, but the folders themselves do not actually exist.&lt;/p&gt;

&lt;p&gt;Understanding this helps explain why operations such as recursively deleting a directory or listing directories are not native object-storage operations. In reality, there are no directories to delete—only objects whose keys share a common prefix.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why "Creating a Directory" Looks Instant
&lt;/h2&gt;

&lt;p&gt;When you create a directory in a traditional filesystem, metadata is written to disk.&lt;/p&gt;

&lt;p&gt;In object storage, creating a directory is often just creating an empty object or simply displaying a prefix in the UI. No real directory structure is created behind the scenes.&lt;/p&gt;

&lt;p&gt;For example, after uploading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;versions/object/g0/file.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;many S3 clients will automatically display:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;versions/
└── object/
    └── g0/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;even though those directories do not actually exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Happens When We List a "Directory"?
&lt;/h2&gt;

&lt;p&gt;Consider the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mc &lt;span class="nb"&gt;ls &lt;/span&gt;s3-pimcore-stg/nbb-pimcore-stg/version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, it looks like we're listing the contents of a directory called &lt;code&gt;version&lt;/code&gt;, but that's not actually what's happening.&lt;/p&gt;

&lt;p&gt;The path can be interpreted as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s3-pimcore-stg   -&amp;gt; configured storage alias/endpoint
nbb-pimcore-stg -&amp;gt; bucket name
version         -&amp;gt; object key prefix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The object storage server does not navigate a directory tree. Instead, it searches for objects whose keys begin with the specified prefix.&lt;/p&gt;

&lt;p&gt;Conceptually, this is 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;Find all objects in bucket "nbb-pimcore-stg"
where key starts with "version"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, if the bucket contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version/object/g0/file1.jpg
version/object/g1/file2.jpg
version/thumbs/file3.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;all of these objects may be returned because they share the same prefix.&lt;/p&gt;

&lt;p&gt;This is not a regular-expression search. It is a &lt;strong&gt;prefix-based lookup&lt;/strong&gt;, which is one of the core concepts used by object storage systems to simulate directories.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Recursive Delete Can Be Expensive
&lt;/h2&gt;

&lt;p&gt;Deleting a directory in a traditional filesystem usually involves traversing the directory tree and removing its contents.&lt;/p&gt;

&lt;p&gt;In object storage, deleting a "directory" is fundamentally different:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;List all objects that share a specific prefix.&lt;/li&gt;
&lt;li&gt;Delete each matching object individually (or in batches).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a prefix containing thousands or millions of objects, this operation can become expensive and time-consuming because the storage system must first discover all matching objects before they can be removed.&lt;/p&gt;

&lt;p&gt;This is another consequence of the fact that object storage does not have real directories—it only has objects and object keys.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Mental model: Treat object storage as a giant key-value store where the key happens to look like a file path. Once you think of it that way, most S3 behaviors become much easier to understand.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Understanding that object storage has &lt;strong&gt;objects and prefixes, not directories&lt;/strong&gt; changes how you design applications around it.&lt;/p&gt;

&lt;p&gt;The same prefix-based lookup that makes a "directory delete" expensive also applies to listing: every time an application "opens a folder," the storage service performs a prefix lookup rather than traversing a real directory tree. For a few dozen objects this is invisible. For millions, it shows up as extra LIST calls, slower responses, and—depending on your provider's pricing model—extra cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design For Prefixes, Not Directories
&lt;/h3&gt;

&lt;p&gt;A common mistake is treating object storage like a traditional filesystem.&lt;/p&gt;

&lt;p&gt;Instead of repeatedly listing prefixes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;versions/
 └── object/
      └── g0/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;consider storing the object keys you already know in a database or index.&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 sql"&gt;&lt;code&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;object_key&lt;/span&gt;
&lt;span class="c1"&gt;---+-----------------------------------&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;versions&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;object&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;g0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;file1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jpg&lt;/span&gt;
&lt;span class="mi"&gt;2&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;versions&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;object&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;g0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;file2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jpg&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can fetch the keys directly and perform batch operations without repeatedly asking the object store to discover them.&lt;/p&gt;

&lt;p&gt;Similarly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prefer batch deletes over deleting objects one by one.&lt;/li&gt;
&lt;li&gt;Avoid repeatedly listing the same prefix in loops.&lt;/li&gt;
&lt;li&gt;Use lifecycle policies when objects have predictable expiration dates.&lt;/li&gt;
&lt;li&gt;Organize object keys so that related objects share predictable prefixes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If your application already knows the object keys, don't ask the storage system to find them again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  A Note About Costs
&lt;/h3&gt;

&lt;p&gt;Many object storage providers charge not only for stored data but also for API requests. AWS S3, for example, charges for LIST, PUT, COPY, and GET operations, while DELETE requests themselves are free. However, deleting a large prefix often requires LIST operations first, which are billable.&lt;/p&gt;

&lt;p&gt;Because of this, good object-storage design is usually about reducing unnecessary discovery operations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Less efficient:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for each user:
    list user folder
    find files
    delete files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;More efficient:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query database for object keys
delete objects in batches
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;S3 even provides a bulk delete API that can remove up to 1,000 objects in a single request, significantly reducing request overhead compared to deleting objects individually.&lt;/p&gt;

&lt;p&gt;A good rule of thumb is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Treat LIST operations as searches and DELETE operations as actions. Searches are often the expensive part, so avoid performing them when you already know the answer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Final Words
&lt;/h2&gt;

&lt;p&gt;Treat object storage as a giant key-value store where the key happens to look like a file path—that one shift in thinking explains most of S3's quirky behavior around directories, listing, and deletes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading:
&lt;/h2&gt;

&lt;p&gt;For a detailed breakdown of storage, request, and transfer pricing, see &lt;a href="https://aws.amazon.com/s3/pricing/?nc=sn&amp;amp;loc=4&amp;amp;refid=aebc39a1-139c-43bb-8354-211ac811b83a" rel="noopener noreferrer"&gt;https://aws.amazon.com/s3/pricing/?nc=sn&amp;amp;loc=4&amp;amp;refid=aebc39a1-139c-43bb-8354-211ac811b83a&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mohammad Ghalambaz&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Backend Engineer&lt;/p&gt;

</description>
      <category>s3</category>
      <category>amazon</category>
      <category>webservices</category>
      <category>objectstorage</category>
    </item>
    <item>
      <title>Listening to the Heartbeat of Your Database: Understanding Change Data Capture (CDC)</title>
      <dc:creator>Mohammad</dc:creator>
      <pubDate>Fri, 19 Jun 2026 12:06:56 +0000</pubDate>
      <link>https://dev.to/calligra/listening-to-the-heartbeat-of-your-database-understanding-change-data-capture-cdc-4kib</link>
      <guid>https://dev.to/calligra/listening-to-the-heartbeat-of-your-database-understanding-change-data-capture-cdc-4kib</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;CDC stands for &lt;strong&gt;Change Data Capture&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What You Will Learn
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What CDC is&lt;/li&gt;
&lt;li&gt;The story behind it&lt;/li&gt;
&lt;li&gt;How it works internally&lt;/li&gt;
&lt;li&gt;Common use cases&lt;/li&gt;
&lt;li&gt;A simplified implementation example&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  What Is CDC?
&lt;/h1&gt;

&lt;p&gt;Change Data Capture (CDC) is a technique for capturing changes made to data and exposing those changes to external systems.&lt;/p&gt;

&lt;p&gt;Instead of repeatedly querying a database to check whether something has changed, CDC allows applications to consume a stream of inserts, updates, and deletes as they happen.&lt;/p&gt;

&lt;p&gt;Think of CDC as a way to listen to the heartbeat of your database.&lt;/p&gt;

&lt;p&gt;Every time data changes, the database records that change internally. CDC makes those changes available so that other systems can react to them in real time.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Story Behind CDC
&lt;/h1&gt;

&lt;p&gt;To understand CDC, it helps to understand how databases have worked for decades.&lt;/p&gt;

&lt;p&gt;Whenever data changes, databases do not simply overwrite the old value and move on. They maintain an internal record of operations that have occurred.&lt;/p&gt;

&lt;p&gt;These records are commonly stored in transaction logs.&lt;/p&gt;

&lt;p&gt;Historically, every database implemented these logs differently, but the purpose was always the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Record every change made to the database&lt;/li&gt;
&lt;li&gt;Recover from failures&lt;/li&gt;
&lt;li&gt;Replicate data to other nodes&lt;/li&gt;
&lt;li&gt;Maintain consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every insert, update, and delete operation generates a log entry.&lt;/p&gt;

&lt;p&gt;These entries are ordered and identified by a unique position or token.&lt;/p&gt;

&lt;p&gt;The ordering is critical.&lt;/p&gt;

&lt;p&gt;Imagine the following sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create User&lt;/li&gt;
&lt;li&gt;Update User&lt;/li&gt;
&lt;li&gt;Delete User&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a replica applied these operations in a different order, it would end up with completely different data.&lt;/p&gt;

&lt;p&gt;Because these logs are ordered, databases can reliably replay changes and reconstruct state over time.&lt;/p&gt;

&lt;p&gt;This is how replication works in many database systems.&lt;/p&gt;

&lt;p&gt;A replica typically starts with an initial snapshot and then continuously applies new changes by replaying the transaction log.&lt;/p&gt;




&lt;h1&gt;
  
  
  Where CDC Comes In
&lt;/h1&gt;

&lt;p&gt;Originally, these logs were intended for the database itself.&lt;/p&gt;

&lt;p&gt;CDC extends that idea by allowing external applications to consume those same changes.&lt;/p&gt;

&lt;p&gt;Instead of only replicas reading database changes, your applications can read them too.&lt;/p&gt;

&lt;p&gt;This means that whenever data changes, you can react immediately without constantly querying the database.&lt;/p&gt;

&lt;p&gt;The underlying idea is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Expose an ordered stream of database changes that external systems can consume reliably.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  How CDC Works
&lt;/h1&gt;

&lt;p&gt;Different databases implement CDC differently, but the concept remains the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  MongoDB
&lt;/h3&gt;

&lt;p&gt;MongoDB provides CDC through &lt;strong&gt;Change Streams&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each event contains a &lt;strong&gt;resume token&lt;/strong&gt;, which allows consumers to continue reading from the last processed change after a restart or failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  MySQL
&lt;/h3&gt;

&lt;p&gt;MySQL exposes changes through its &lt;strong&gt;Binary Log (Binlog)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;CDC tools can read the binlog and transform database operations into events.&lt;/p&gt;

&lt;h3&gt;
  
  
  PostgreSQL
&lt;/h3&gt;

&lt;p&gt;PostgreSQL provides CDC through &lt;strong&gt;Logical Decoding&lt;/strong&gt; and &lt;strong&gt;Logical Replication&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;These mechanisms allow applications to consume database changes in order.&lt;/p&gt;

&lt;p&gt;Although the implementations differ, they all provide the same capability:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An ordered stream of database changes.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  CDC Is Usually Pull-Based
&lt;/h1&gt;

&lt;p&gt;One common misconception is that databases push changes directly to your application.&lt;/p&gt;

&lt;p&gt;In reality, most CDC implementations are fundamentally pull-based.&lt;/p&gt;

&lt;p&gt;The consumer requests the next available change from the database.&lt;/p&gt;

&lt;p&gt;However, applications typically maintain a long-lived connection or cursor, making the experience feel very similar to receiving pushed events.&lt;/p&gt;

&lt;p&gt;For example, MongoDB's &lt;code&gt;watch()&lt;/code&gt; API keeps a stream open and continuously delivers new events as they become available.&lt;/p&gt;

&lt;p&gt;From the application's perspective, it feels real-time.&lt;/p&gt;




&lt;h1&gt;
  
  
  A Simplified CDC Example
&lt;/h1&gt;

&lt;p&gt;To understand the concept, imagine that every database change is converted into a standardized event.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;captureDataChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$before&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$cdcEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"metadata"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s2"&gt;"operation"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"table"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"id"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s2"&gt;"before"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$before&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"after"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$after&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Streaming event: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cdcEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;captureDataChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"UPDATE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"role"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Developer"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"role"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Architect"&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;Output:&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;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UPDATE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"table"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&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;span class="nl"&gt;"before"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Developer"&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;span class="nl"&gt;"after"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Architect"&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;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;Real CDC systems work differently under the hood, but the idea is very similar:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detect a database change&lt;/li&gt;
&lt;li&gt;Convert it into an event&lt;/li&gt;
&lt;li&gt;Publish it to interested consumers&lt;/li&gt;
&lt;/ol&gt;




&lt;h1&gt;
  
  
  CDC vs Traditional Polling
&lt;/h1&gt;

&lt;p&gt;Without CDC, applications often use polling.&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 sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_seen_timestamp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach works, but it has drawbacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Additional load on the database&lt;/li&gt;
&lt;li&gt;Expensive queries at scale&lt;/li&gt;
&lt;li&gt;Possibility of missing updates&lt;/li&gt;
&lt;li&gt;Duplicate processing logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CDC avoids these problems by consuming changes directly from the database's change stream.&lt;/p&gt;

&lt;p&gt;Instead of repeatedly asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Has anything changed?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;the database tells you:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This is what changed."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Why CDC Matters
&lt;/h1&gt;

&lt;p&gt;CDC is more than a database feature.&lt;/p&gt;

&lt;p&gt;It is an architectural building block.&lt;/p&gt;

&lt;p&gt;Once database changes become events, many new possibilities emerge.&lt;/p&gt;

&lt;p&gt;Common use cases include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search index synchronization (Elasticsearch/OpenSearch)&lt;/li&gt;
&lt;li&gt;Analytics pipelines&lt;/li&gt;
&lt;li&gt;Event-driven architectures&lt;/li&gt;
&lt;li&gt;Cache invalidation&lt;/li&gt;
&lt;li&gt;Audit trails&lt;/li&gt;
&lt;li&gt;Data warehouse synchronization&lt;/li&gt;
&lt;li&gt;Building materialized views&lt;/li&gt;
&lt;li&gt;Synchronizing data across services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, when a product is updated in your database, a CDC consumer can automatically update:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search indexes&lt;/li&gt;
&lt;li&gt;Caches&lt;/li&gt;
&lt;li&gt;Reporting systems&lt;/li&gt;
&lt;li&gt;Recommendation engines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;without requiring changes to the original application.&lt;/p&gt;




&lt;h1&gt;
  
  
  CDC in Modern Architectures
&lt;/h1&gt;

&lt;p&gt;Many organizations use CDC platforms such as Debezium to provide a unified way of consuming changes across different databases.&lt;/p&gt;

&lt;p&gt;These platforms often publish CDC events into systems such as Apache Kafka, where multiple consumers can process the same stream independently.&lt;/p&gt;

&lt;p&gt;This makes CDC a key building block in modern event-driven systems.&lt;/p&gt;




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

&lt;p&gt;At its core, CDC is a simple idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every database change is already being recorded somewhere. CDC allows external systems to listen to those changes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Whether it is MongoDB Change Streams, MySQL Binlogs, or PostgreSQL Logical Replication, the principle remains the same:&lt;/p&gt;

&lt;p&gt;An ordered stream of database changes that can be consumed reliably.&lt;/p&gt;

&lt;p&gt;Once you start viewing database updates as events rather than rows being modified, CDC becomes one of the most powerful tools for building scalable and loosely coupled systems.&lt;/p&gt;

</description>
      <category>cdc</category>
      <category>database</category>
      <category>architecture</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>From Notion to Everywhere: Building Calligra</title>
      <dc:creator>Mohammad</dc:creator>
      <pubDate>Thu, 18 Jun 2026 18:11:58 +0000</pubDate>
      <link>https://dev.to/calligra/from-notion-to-everywhere-building-calligra-9p6</link>
      <guid>https://dev.to/calligra/from-notion-to-everywhere-building-calligra-9p6</guid>
      <description>&lt;p&gt;Every developer blog needs a first post.&lt;/p&gt;

&lt;p&gt;Mine starts with a pen, a family name, and a workflow I got tired of doing by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Publishing Problem
&lt;/h2&gt;

&lt;p&gt;I write almost everything in Notion.&lt;/p&gt;

&lt;p&gt;It is where ideas begin, drafts evolve, and articles eventually take shape. But publishing those articles was always the same repetitive process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copy the content from Notion.&lt;/li&gt;
&lt;li&gt;Fix formatting issues.&lt;/li&gt;
&lt;li&gt;Paste it into my blog.&lt;/li&gt;
&lt;li&gt;Repeat for &lt;a href="http://dev.to/"&gt;Dev.to&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Repeat for Hashnode.&lt;/li&gt;
&lt;li&gt;Repeat for Medium.&lt;/li&gt;
&lt;li&gt;Upload cover images multiple times.&lt;/li&gt;
&lt;li&gt;Configure canonical URLs manually.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these steps were difficult.&lt;/p&gt;

&lt;p&gt;They were simply repetitive.&lt;/p&gt;

&lt;p&gt;And as developers, repetitive work usually means there is an opportunity for automation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;

&lt;p&gt;I wanted a publishing workflow that looked 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;Notion → Calligra → Personal Blog + Dev.to + Hashnode + Medium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write once.&lt;/p&gt;

&lt;p&gt;Publish everywhere.&lt;/p&gt;

&lt;p&gt;Keep my own domain as the source of truth.&lt;/p&gt;

&lt;p&gt;That simple idea became Calligra.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Calligra?
&lt;/h2&gt;

&lt;p&gt;Calligra is an open-source CLI tool that takes content from Notion and publishes it to multiple destinations with a single command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make &lt;span class="nb"&gt;sync
&lt;/span&gt;make build
make publish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behind the scenes, Calligra:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetches content from the Notion API&lt;/li&gt;
&lt;li&gt;Converts the content into platform-specific formats&lt;/li&gt;
&lt;li&gt;Uploads images and assets&lt;/li&gt;
&lt;li&gt;Publishes to supported platforms&lt;/li&gt;
&lt;li&gt;Sets canonical URLs back to your own website&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Your personal blog owns the content. Other platforms help distribute it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Screenshot: Publishing Workflow
&lt;/h2&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb38d05ls8joavmpvm05b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb38d05ls8joavmpvm05b.png" alt="Blog Asset" width="800" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Content Flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────┐
│  Notion  │
└────┬─────┘
     │
     ▼
┌──────────┐
│ Calligra │
└────┬─────┘
     │
     ├──► Personal Blog
     ├──► Dev.to
     ├──► Hashnode
     └──► Medium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Story Behind the Name
&lt;/h2&gt;

&lt;p&gt;Before the code, there is the name.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Craft
&lt;/h3&gt;

&lt;p&gt;The word &lt;em&gt;Calligra&lt;/em&gt; comes from the same roots as &lt;em&gt;calligraphy&lt;/em&gt; — the art of beautiful writing.&lt;/p&gt;

&lt;p&gt;Calligraphy is not just about words.&lt;/p&gt;

&lt;p&gt;It is about intention, structure, and craftsmanship.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Heritage
&lt;/h3&gt;

&lt;p&gt;My family name is &lt;strong&gt;Ghalambaz (قلم‌باز)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A rough translation would be:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Pen player."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Historically, it referred to someone who practiced writing as both a craft and an art.&lt;/p&gt;

&lt;p&gt;Someone who enjoyed working with words.&lt;/p&gt;

&lt;p&gt;When I built a tool dedicated entirely to writing and publishing, the name felt inevitable.&lt;/p&gt;

&lt;p&gt;Calligra connects a modern publishing workflow with a much older tradition of writing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Open Source from Day One
&lt;/h2&gt;

&lt;p&gt;Although Calligra started as a personal tool, I built it to be useful for other developers as well.&lt;/p&gt;

&lt;p&gt;If you write in Notion or anywhere else and publish technical content, you might find it useful.&lt;/p&gt;

&lt;p&gt;The project is open source, and contributions, feedback, and ideas are always welcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt;  &lt;a href="https://github.com/ghalambaz/Calligra" rel="noopener noreferrer"&gt;&lt;strong&gt;Ghalambaz/Calligra&lt;/strong&gt;&lt;/a&gt; &lt;/p&gt;




&lt;h2&gt;
  
  
  Why Start This Blog?
&lt;/h2&gt;

&lt;p&gt;Calligra solves the publishing problem.&lt;/p&gt;

&lt;p&gt;Now it's time to focus on the writing.&lt;/p&gt;

&lt;p&gt;This blog will be a place for practical engineering lessons, architectural decisions, debugging stories, and lessons learned from building software in production.&lt;/p&gt;

&lt;p&gt;Not tutorials copied from documentation.&lt;/p&gt;

&lt;p&gt;Not generic AI-generated content.&lt;/p&gt;

&lt;p&gt;I do use AI—but as an editor, not an author.&lt;/p&gt;

&lt;p&gt;AI is great at fact-checking, catching mistakes, improving clarity, challenging assumptions, and helping me communicate ideas more effectively. The experiences, opinions, successes, and failures will still be my own.&lt;/p&gt;

&lt;p&gt;The goal is simple: share real engineering stories, real production problems, and solutions that might help someone else avoid the same mistakes.&lt;/p&gt;




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

&lt;p&gt;This post marks the beginning of the journey, not the destination.&lt;/p&gt;

&lt;p&gt;Over time, I'll be writing about software architecture, backend engineering, distributed systems, debugging sessions, performance bottlenecks, deployment lessons, and the occasional mistake that taught me something valuable.&lt;/p&gt;

&lt;p&gt;Some articles will be technical deep dives.&lt;/p&gt;

&lt;p&gt;Some will be lessons learned from real projects.&lt;/p&gt;

&lt;p&gt;Some may simply document a problem that took far longer to solve than it should have.&lt;/p&gt;

&lt;p&gt;The goal is not to publish content for the sake of publishing.&lt;/p&gt;

&lt;p&gt;The goal is to share experiences, capture lessons before they are forgotten, and hopefully help other engineers who run into similar challenges along the way.&lt;/p&gt;




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

&lt;p&gt;Calligra started as a way to eliminate a repetitive workflow.&lt;/p&gt;

&lt;p&gt;Now it has become the foundation of this blog.&lt;/p&gt;

&lt;p&gt;If you are a developer who writes in Notion and owns a personal domain, I hope it saves you time as well.&lt;/p&gt;

&lt;p&gt;Thanks for stopping by.&lt;/p&gt;

&lt;p&gt;See you in the next post.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Mohammad Ghalambaz&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;writing at Calligra.dev&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blogging</category>
      <category>developerworkflow</category>
      <category>publishing</category>
    </item>
  </channel>
</rss>
