<?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: Denys Shulhin</title>
    <description>The latest articles on DEV Community by Denys Shulhin (@ej_snowden).</description>
    <link>https://dev.to/ej_snowden</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%2F4140801%2F25849a05-131b-44cd-8a0f-e3ab0f21105f.jpg</url>
      <title>DEV Community: Denys Shulhin</title>
      <link>https://dev.to/ej_snowden</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ej_snowden"/>
    <language>en</language>
    <item>
      <title>I thought clipboard sync would be simple. Android had other plans.</title>
      <dc:creator>Denys Shulhin</dc:creator>
      <pubDate>Thu, 24 Sep 2026 09:01:25 +0000</pubDate>
      <link>https://dev.to/ej_snowden/i-thought-clipboard-sync-would-be-simple-android-had-other-plans-ege</link>
      <guid>https://dev.to/ej_snowden/i-thought-clipboard-sync-would-be-simple-android-had-other-plans-ege</guid>
      <description>&lt;p&gt;I started ClipboardX because of a very small annoyance.&lt;/p&gt;

&lt;p&gt;While developing and testing Android apps, I was constantly moving things between my phone and my Windows PC:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;logs&lt;/li&gt;
&lt;li&gt;URLs&lt;/li&gt;
&lt;li&gt;error messages&lt;/li&gt;
&lt;li&gt;screenshots&lt;/li&gt;
&lt;li&gt;bits of text I needed on the other device&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My workflow was often:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Copy -&amp;gt; Telegram/WhatsApp -&amp;gt; send to myself -&amp;gt; open on PC -&amp;gt; copy again.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once or twice, that's nothing.&lt;/p&gt;

&lt;p&gt;Do it several times a day for months and it becomes surprisingly annoying.&lt;/p&gt;

&lt;p&gt;So I had what seemed like a very simple idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Copy something on Android and have it available on Windows.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I thought this would be a small side project.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The original plan
&lt;/h2&gt;

&lt;p&gt;My first mental architecture was roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Android background process
          |
          v
       Backend
          |
          v
     Windows app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the clipboard changes, send the new value.&lt;/p&gt;

&lt;p&gt;Done.&lt;/p&gt;

&lt;p&gt;Except Android has very good reasons for not allowing arbitrary applications to sit in the background reading everything a user copies.&lt;/p&gt;

&lt;p&gt;And suddenly the central feature of my clipboard app was fighting the operating system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The keyboard
&lt;/h2&gt;

&lt;p&gt;Eventually I realized I needed a different approach.&lt;/p&gt;

&lt;p&gt;Instead of treating ClipboardX only as a background utility, I integrated clipboard functionality into an Android keyboard.&lt;/p&gt;

&lt;p&gt;That gave the product a natural place where clipboard access actually makes sense: while the user is typing.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F59tujh7wh73yb7rfehhk.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F59tujh7wh73yb7rfehhk.png" alt=" " width="640" height="882"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also created an entirely new problem.&lt;/p&gt;

&lt;p&gt;I had never intended to build a keyboard.&lt;/p&gt;

&lt;p&gt;A keyboard sounds manageable until you start thinking about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;layouts&lt;/li&gt;
&lt;li&gt;languages&lt;/li&gt;
&lt;li&gt;predictions&lt;/li&gt;
&lt;li&gt;special keys&lt;/li&gt;
&lt;li&gt;symbols&lt;/li&gt;
&lt;li&gt;input behavior&lt;/li&gt;
&lt;li&gt;keyboard lifecycle&lt;/li&gt;
&lt;li&gt;different screen sizes&lt;/li&gt;
&lt;li&gt;all the tiny interactions people expect from something they use hundreds of times per day&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At one point this nearly killed the project.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3k1ypumn7zmcf53uljvy.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3k1ypumn7zmcf53uljvy.png" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was spending more time thinking about how to build a decent keyboard than about the problem I originally wanted to solve.&lt;/p&gt;

&lt;p&gt;What eventually got me unstuck also became one of the most useful lessons I learned from this project:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I didn't need to build every layer myself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I found an existing open-source keyboard foundation that I could legally adapt and build on, then focused my work on the ClipboardX-specific functionality.&lt;/p&gt;

&lt;p&gt;My problem wasn't:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do I build the best Android keyboard?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do I make clipboard synchronization useful and reliable?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction saved a huge amount of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then "send the clipboard" stopped being simple too
&lt;/h2&gt;

&lt;p&gt;Once I had Android and Windows talking to each other, the architecture looked deceptively straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Device A
   |
   | clipboard update
   v
Backend
   |
   | relay
   v
Device B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first successful transfer was a great moment.&lt;/p&gt;

&lt;p&gt;Then I started asking annoying questions.&lt;/p&gt;

&lt;p&gt;What if Device B disconnects halfway through?&lt;/p&gt;

&lt;p&gt;What if the connection dies immediately after receiving a message?&lt;/p&gt;

&lt;p&gt;What if I retry and deliver it twice?&lt;/p&gt;

&lt;p&gt;What if Android goes into the background?&lt;/p&gt;

&lt;p&gt;What if the destination applies the clipboard value but fails before acknowledging it?&lt;/p&gt;

&lt;p&gt;What if the same message appears again after reconnecting?&lt;/p&gt;

&lt;p&gt;What if 100 clipboard changes happen quickly?&lt;/p&gt;

&lt;p&gt;At that point, "send this string to another device" had turned into a small distributed systems problem.&lt;/p&gt;

&lt;p&gt;The system eventually needed concepts like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transfer IDs&lt;/li&gt;
&lt;li&gt;acknowledgements&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;deduplication&lt;/li&gt;
&lt;li&gt;reconnect handling&lt;/li&gt;
&lt;li&gt;delivery state&lt;/li&gt;
&lt;li&gt;accounting that doesn't charge twice for the same transfer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One particularly annoying class of bugs came from situations where the clipboard had actually arrived at the destination, but the acknowledgement never made it back.&lt;/p&gt;

&lt;p&gt;From the user's perspective:&lt;/p&gt;

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

&lt;p&gt;From the backend's perspective:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Those are the bugs that made me appreciate observability much more than I did before this project.&lt;/p&gt;

&lt;h2&gt;
  
  
  I also overengineered it
&lt;/h2&gt;

&lt;p&gt;At one stage I designed a transport "ladder".&lt;/p&gt;

&lt;p&gt;The idea was that ClipboardX could try multiple ways of moving data between devices and choose the fastest available path.&lt;/p&gt;

&lt;p&gt;I experimented with a transport ladder involving WebRTC/P2P, TURN, QUIC and fallback paths.&lt;/p&gt;

&lt;p&gt;On paper, it was much cooler than a simple relay.&lt;/p&gt;

&lt;p&gt;In practice, I was building complexity before I had earned the need for it.&lt;/p&gt;

&lt;p&gt;Today the production path is intentionally much simpler.&lt;/p&gt;

&lt;p&gt;I would rather have one boring transport that works reliably than several clever transports that fail in interesting ways.&lt;/p&gt;

&lt;p&gt;I may revisit the more advanced transport architecture later, but only after the basic path is extremely reliable.&lt;/p&gt;

&lt;p&gt;That has probably been my biggest architectural lesson from ClipboardX:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Design so complexity can be added later. Don't implement all of that complexity on day one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Images made it interesting again
&lt;/h2&gt;

&lt;p&gt;Text clipboard synchronization is relatively small.&lt;/p&gt;

&lt;p&gt;Then I added images.&lt;/p&gt;

&lt;p&gt;Now I wasn't just moving a JSON payload containing a string.&lt;/p&gt;

&lt;p&gt;A screenshot can be several megabytes. A photo can be much larger.&lt;/p&gt;

&lt;p&gt;That introduces another set of questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;payload sizes&lt;/li&gt;
&lt;li&gt;transfer handling&lt;/li&gt;
&lt;li&gt;memory usage&lt;/li&gt;
&lt;li&gt;interrupted transfers&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;performance on weaker devices&lt;/li&gt;
&lt;li&gt;what happens when one side disappears halfway through&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Again, the visible feature was tiny:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Copy image here -&amp;gt; image appears there.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The amount of machinery behind that interaction was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting it working wasn't the same as shipping it
&lt;/h2&gt;

&lt;p&gt;This was probably the bigger lesson for me.&lt;/p&gt;

&lt;p&gt;I've built personal projects before where the interesting technical part worked, I proved the idea to myself, and then I gradually stopped working on them.&lt;/p&gt;

&lt;p&gt;The first 80% is fun.&lt;/p&gt;

&lt;p&gt;The remaining 20% is often:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;weird edge cases&lt;/li&gt;
&lt;li&gt;settings screens&lt;/li&gt;
&lt;li&gt;reconnect bugs&lt;/li&gt;
&lt;li&gt;installers&lt;/li&gt;
&lt;li&gt;billing&lt;/li&gt;
&lt;li&gt;translations&lt;/li&gt;
&lt;li&gt;monitoring&lt;/li&gt;
&lt;li&gt;store requirements&lt;/li&gt;
&lt;li&gt;deployment&lt;/li&gt;
&lt;li&gt;migrations&lt;/li&gt;
&lt;li&gt;privacy policies&lt;/li&gt;
&lt;li&gt;hundreds of tiny UI problems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ClipboardX had the same phase.&lt;/p&gt;

&lt;p&gt;There were several points where I genuinely considered dropping it.&lt;/p&gt;

&lt;p&gt;But this time I wanted to see what happened if I kept going after the fun prototype stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ClipboardX looks like today
&lt;/h2&gt;

&lt;p&gt;After roughly a year of working on it on and off, ClipboardX is now actually in production.&lt;/p&gt;

&lt;p&gt;It currently has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an Android application&lt;/li&gt;
&lt;li&gt;an Android keyboard with clipboard access&lt;/li&gt;
&lt;li&gt;a Windows application&lt;/li&gt;
&lt;li&gt;two-way text and image synchronization&lt;/li&gt;
&lt;li&gt;local clipboard history&lt;/li&gt;
&lt;li&gt;QR device pairing&lt;/li&gt;
&lt;li&gt;end-to-end encrypted transfers&lt;/li&gt;
&lt;li&gt;no required user account&lt;/li&gt;
&lt;li&gt;backend/cloud infrastructure behind the device communication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is available on Google Play and the Microsoft Store.&lt;/p&gt;

&lt;p&gt;The funny part is that the product still looks conceptually almost identical to the sentence I started with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Copy on Android. Paste on Windows.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most of the work went into making that sentence boring enough that the user doesn't have to think about what happens underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;If I started again today, I would build the simplest reliable path first.&lt;/p&gt;

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

&lt;p&gt;No premature optimization for every possible network topology.&lt;/p&gt;

&lt;p&gt;No solving hypothetical scale problems before I had users.&lt;/p&gt;

&lt;p&gt;I would still think about extensibility, but there is a big difference between:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This architecture shouldn't prevent us from adding X later.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;blockquote&gt;
&lt;p&gt;Let's implement X now because we might need it someday.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;ClipboardX taught me that difference the expensive way.&lt;/p&gt;

&lt;p&gt;I'd also spend more time testing the boring failure paths earlier.&lt;/p&gt;

&lt;p&gt;The happy path is usually easy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;send
-&amp;gt; receive
-&amp;gt; success
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting problems live here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;send
-&amp;gt; receive
-&amp;gt; connection dies
-&amp;gt; acknowledgement disappears
-&amp;gt; reconnect
-&amp;gt; retry
-&amp;gt; is this a duplicate?
-&amp;gt; did we already charge for it?
-&amp;gt; what state is the UI supposed to show?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those scenarios ended up mattering much more than making the first demo look fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The main lesson
&lt;/h2&gt;

&lt;p&gt;ClipboardX started as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I don't want to send things to myself on Telegram anymore.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It turned into my first project where I had to think about the whole path from an idea to something people can actually install and use:&lt;/p&gt;

&lt;p&gt;Android, Windows, backend infrastructure, reliability, deployment, billing, stores, monitoring and support.&lt;/p&gt;

&lt;p&gt;And I think that is what changed the project for me.&lt;/p&gt;

&lt;p&gt;Getting code to work is one problem.&lt;/p&gt;

&lt;p&gt;Getting a product to keep working outside your machine is a very different one.&lt;/p&gt;




&lt;p&gt;If you're curious, ClipboardX is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://clipboardx.it.com/" rel="noopener noreferrer"&gt;https://clipboardx.it.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm still developing it myself, and I'm especially curious how other developers would approach the same problems - particularly Android's clipboard restrictions and reliable cross-device delivery.&lt;/p&gt;

&lt;p&gt;Would you keep the architecture intentionally relay-based, or eventually bring P2P transports back once the basic path is stable?&lt;/p&gt;

</description>
      <category>android</category>
      <category>programming</category>
      <category>architecture</category>
      <category>devjournal</category>
    </item>
  </channel>
</rss>
