Most online PDF tools quietly remove the selectable text from your documents. I discovered this while testing four popular services. A contract I uploaded came back as a stack of flat images. The core pdf merge lessons I learned that day changed how I build software. So I built my own privacy-first tool in about seven hours. Here are the main takeaways from that process.

Lesson 1: The Rasterization Trap Nobody Warns You About
You upload a signed contract with form fields. You merge it with an addendum. The download looks fine on screen. But try to search for a specific clause. Nothing works. The text is completely gone. This happens because tools like Smallpdf, iLovePDF, and PDF24 use server-side rendering tools in their default flow. These tools rasterize the PDF. They turn every page into a static image.
The raster pipeline often relies on libraries like Cairo or headless Chromium. These systems render the PDF into a high-resolution image and then wrap it back into a PDF container. The text layer is lost because it was rendered to pixels first. File sizes can increase by 300 percent or more. Bookmarks vanish. Form fields flatten. For a privacy-focused tool, accepting this trade-off felt wrong from the start. One of the first pdf merge lessons I learned was to question whether a service actually preserves your data or just displays it.
Lesson 2: The Unsexy Power of Existing Open-Source Tools
Reinventing the wheel is a common mistake for builders. My first instinct was to find a Go library that could manipulate PDF objects directly. But the smartest move was stepping back. The Poppler-utils package has shipped a binary called pdfunite for over a decade. It does one thing well. It opens each input PDF as an object graph, appends the pages in order, fixes up the cross-reference table, and writes the result.
It does not rasterize. It does not recompress images. It does not strip out embedded fonts. The output is byte-for-byte equivalent to the inputs for all the important parts. Large companies could use this approach. They choose not to because their pipelines are already built around a raster flow. Adding a separate structural merge path means more code to maintain. For a solo project, choosing the unsexy tool that just works was a massive time saver. Poppler-utils is a fork of Xpdf, maintained by freedesktop.org. The pdfunite binary operates at the structure level and manipulates the cross-reference stream directly.
Lesson 3: The 50-Line Wrapper That Handles the Heavy Lifting
Bridging Go to pdfunite took about 50 lines of standard library subprocess code. The structure is straightforward. A NewPDFUnite function checks for the binary at startup using exec.LookPath. If the binary is missing from the PATH, the service fails immediately. This fail-fast approach prevents silently broken merges later in production.
A hard timeout of 120 seconds wraps every merge call using the context package. This deadline ensures that a massive twenty-file merge cannot hang the system forever. Standard error output is captured into a buffer. If the subprocess fails, the error message surfaces directly to the user instead of vanishing into a log file. The Go wrapper uses exec.CommandContext, which provides clean cancellation. If the HTTP request is cancelled, the subprocess is killed immediately. Another key pdf merge lesson is that a simple wrapper around a stable binary often beats a complex native library.
Lesson 4: The Zero-Migration Data Model Trick
Changing a database schema always comes with risk. Migrations can lock tables. They can break running queries. For this feature, I wanted to avoid touching the database schema entirely. The existing conversion_jobs table had columns for source_format and target_format. For a standard image conversion, these might be jpg and webp.
For the merge feature, the source format is pdf. The target format is pdf-merge. This synthetic identifier prevents the system from accidentally routing a merge job into the image conversion pipeline. Zero migration. Zero new columns. Zero data model debt. This approach works because the table already tracks user IDs, file sizes, processing duration, output paths, page counts, and expiration timestamps. The merge job simply fills the same fields with merge-specific values. The existing CleanupExpiredJobs goroutine scans for expires_at values that have passed. No new table, no new cleanup logic. This particular pdf merge lesson shows that you often have everything you need already.
Lesson 5: Why Fewer Code Paths Is a Feature
The temptation to build a dedicated merge pipeline was strong. A separate database table. A separate processing queue. A separate frontend handler. But for a solo founder, every new code path is a tax. It demands tests. It demands monitoring. It demands documentation. Choosing to reuse the existing convert flow for merges felt liberating.
You may also enjoy reading: 5 Features Claude Pro Needs to Be Close to Perfect.
The whole feature ships with a parallel MergePDFs service method and a new HTTP handler. Everything else is shared. The cleanup goroutine that removes expired files after one hour works for merges too. The temporary file storage logic is identical. Adding a second queue system, such as RabbitMQ or Redis, would have been overkill. The existing in-process job pool handles merges and conversions seamlessly. Consistency across the codebase reduces cognitive load. This pattern extends cleanly to upcoming features like image-to-PDF conversion and OCR. Having one less code path is a feature, not a downside.
Lesson 6: Designing for the User Who Walks Away
Many PDF tools require an account. They store your files indefinitely. They analyze your documents to train their models. A privacy-first approach requires a different mindset. Every merge result gets a one-hour expiration time. The cleanup goroutine sweeps expired files continuously. No user account is required to upload or merge documents.
The technical implementation is simple. A timestamp column called expires_at stores the deadline. A background loop checks for expired rows and deletes the associated files. But the product signal is huge. It tells the user that their contract, their financial statement, or their personal form will not live on a server forever. No user accounts mean no password hashing, no session management, and no GDPR deletion requests to manually handle. The most important pdf merge lessons are not always about code. Sometimes they are about trust. Anonymity is a technical feature that requires deliberate architectural choices.
Lesson 7: What a Tight Timebox Does for Your Focus
Seven hours is not a lot of time. It forces you to skip the nice-to-haves. I did not build a drag-and-drop reordering interface. I did not add batch merging with custom page ranges. I did not write extensive unit tests for the subprocess wrapper. I focused on the critical path. Uploading two PDFs, merging them structurally, and returning the result with searchable text intact was the only goal.
The time constraint also prevented over-engineering. Instead of writing a complex PDF manipulation library, I used pdfunite. Instead of designing a new database schema, I reused the existing one. Instead of building a dedicated file processing pipeline, I added one route handler. The core loop was simple. Accept files, pass them to the binary, return the result. The lesson is clear. A tight deadline forces you to ask what your users actually need versus what you think they need. Speed and simplicity often go hand in hand.
Building a privacy-first tool in a single day taught me that most complexity comes from choices, not from the problem itself. Choosing the right binary, reusing an existing schema, and limiting the scope to one clean code path made the difference between a weekend project and a never-ending rewrite. The next time I need a feature, I will look for the unsexy answer first.






