Skip to main content

Command Palette

Search for a command to run...

The Best Code is Unwritten: Architectural Lessons from Startup Production (May 2026)

Updated
7 min read
The Best Code is Unwritten: Architectural Lessons from Startup Production (May 2026)
F
Software Engineer. Dark Souls | Stoicism | FullStack Engineering | Christianism | INFJ

I started building for startups in 2023. By 2026, I was migrating live auth systems, cutting query latency by 79%, and learning that the hardest part of engineering isn’t shipping code, it’s deciding what code should not be written.

This article is a compilation of practical architectural lessons and counterintuitive insights I’ve gathered in the trenches this year, up to May 25, 2026.

Don't reinvent the wheel with auth

I still remember the time when I finished writing the main auth implementation for our main platform at work. I was proud of myself for using custom JWTs and refresh token mechanisms, and for spending hours building the authentication feature.

However, days later, after launching our MVP, I got a new requirement:

"Now the system must be able to support OAuth with Google."

How was I able to tell my CEO that our authentication system was held together with duct tape and prayers? I decided to migrate the authentication system from a custom JWT implementation to a dedicated library and migrate the stateless auth to sessions.

You can implement auth in 3 different ways in a system:

  1. Building it yourself and dealing with HIPAA, CSRF, refresh tokens, stateless vs stateful auth, whatnot. You'll be spending more time building and testing this than shipping features.

  2. Using dedicated libraries to build it. Such as BetterAuth, Passport, Laravel Sanctum, etc. It prevents vendor lock-in; however, you must use a secure and battle-tested option.

  3. Using services like Auth0 and Clerk. While this option is valid for solopreneurs or side projects, companies and organizations are skeptical about sharing their information and user base with a third-party provider.

If you’re thinking, "I’ll just build it myself," please don’t. Unless you have built authentication systems at a corporate level from scratch and you work closely with a security engineer, you'll struggle with ALL the edge cases during the process.

Don't rewrite, embrace the system

An overengineered system working in production is better than a rewrite.

I was talking with a client about a rewrite because we're automating too many things in our CI/CD pipeline, and we have OpenAPI 3.1 documentation for everything. Yes, it's kind of overkill for a small product, but it'll allow us to go faster with the time.

We're now focused on delivering features and scaling the system. Migrating our decoupled architecture to a monolith is a task with no benefits because we're still finding PMF, and our system works with 99.995% uptime. At the end, we only rewrote the authentication module for compliance purposes.

Yes, the developer experience is just good, but the team and I know the trade-offs made and where the system bottlenecks are. It's not worth doing a rewrite when you're finding PMF, you're a small team, and there are no clear benefits from it.

Write tests for every bug you fix

A stakeholder asked me why users could not register again in the app once they deleted their account. I verified the database schema, and it confirmed we had a unique constraint for the user email. I removed it, since we're dealing with soft deletes in a multi-tenant system.

I ran the tests; 6 verifications and checks broke in the system since we had not tested this rare edge case that our stakeholder found. I fixed the use cases and wrote a new test to verify that this specific edge case never happens again. All tests were green again.

Embrace incremental migrations

Eventually, you'll have to learn this lesson. Avoid running scripts or commands to make database changes in a production system.

Use incremental migration instead of using cron jobs or when the user performs a specific system operation.

Let's say you need to rewrite the authentication system to use accounts.

Instead of migrating the entire user data and passwords to the new tables, you can do the following:

  • Whenever a new user signs in, you create an account and link their existing password and authentication method to the new table. This allowed us to migrate 23% of our existing user base in a single day.

  • Write tests to verify legacy users can sign in properly. You should not modify the hashing algorithm and system internals used to perform this process, only the way you're retrieving and saving data.

  • You can use dual-writes, but research the trade-offs and see if they fit your system requirements.

  • The sign-up method is implemented using the new logic because it is a fresh write operation.

Communicate well with stakeholders

Here are a few rules to communicate with your non-technical manager and stakeholders:

  • They care about results and deadlines. Speak using results and simple language. (e.g., 'We’re updating how we store deleted users so reactivation doesn’t fail. ETA: Thursday. ' don't say things like 'We need to refactor the DB schema. ')

  • Learn how to say no without using the word "no". (e.g., "We can build a mobile app in the future, but for now, let's focus on getting 200 DAU and converting 8% of them to our premium plan)

  • State the why, not the how (e.g., "I fixed the bug to upload images, now the onboarding step will convert more users")

  • Master Git, conventional commits, and technical writing/documentation. Even if you work alone, you need to be able to analyze your own work for future reports.

  • Document your work consistently. Not for a promotion, for mastery.

Building is easy, maintaining is not

How I wish we could go back to building the same system again using Next.js/Laravel and keep things simple.

Yet actual users and businesses now rely on the platform we built using decoupled architecture. This means we introduced DevOps and development overhead using a separate backend and frontend, but now we can build for scale.

My advice here is to maintain a project, any kind of software development project. For months and even years, if you can. It's going to teach a lot about your tech stack's weaknesses, how to pay technical debt, deal with legacy code, and how actual users interact with the application.

Underrated skills you need in a startup

Forget about React, K8s, Java/Spring, or microservices for a second. I recommend learning this well before working in a startup:

  • IaC (Pulumi, Terraform - you need new and existing environments to be replicated using code)

  • A framework to build monolithic applications (Laravel, Next.js, Django - microservices and decoupled architectures are not everything)

  • DevOps (automating deployments - so you can focus on building)

  • How to create technical documentation for your work (you can have a document of achievements in Google Docs or Notion)

  • Linux, SQL, and PostgreSQL (2am emergencies, restoring a database from a backup...)

  • Testing (unit tests, E2E tests, and integration tests - Playwright, Vitest, and similar frameworks).

  • Git, conventional commits, branches, PRs, merges, and how to use version control well.

Conclusion

Software engineering is not about finding the perfect architecture; it is about choosing which trade-offs you are willing to live with.

None of the insights shared here are dogmatic rules. Every startup faces unique bottlenecks, and the stack or strategy you choose must strictly serve your current business landscape.

However, if there is one universal truth I’ve learned from maintaining systems in production, it’s that the tools you use matter far less than your discipline. You need to focus on building systems that allow you to sleep at night while production runs seamlessly.

Choose your technical debt wisely, document your decisions, and play the long game.