Every developer who's moved between a large enterprise and a smaller team has felt the whiplash. You open a pull request that would take two days to ship at a startup, and it takes two sprints at BigCorp. The code itself looks different too — not because the engineers are worse, but because the incentives are completely different.
There's been a lot of discussion lately about why talented engineers end up writing mediocre code at large companies. The short answer? It's not about skill. It's about the system they're operating in. And nowhere is this more visible than in how teams handle authentication.
Let me use auth as a lens to compare these two worlds — and look at the actual tools and tradeoffs involved.
Why Big Companies Produce Bloated Code
At large companies, code doesn't just need to work. It needs to survive committee review, satisfy platform team requirements, handle edge cases from three acquisitions ago, and avoid breaking a service that another team depends on but nobody documented.
The result? Over-abstraction. Defensive coding taken to an extreme. Layers upon layers of indirection that exist not because they're architecturally sound, but because they're politically safe.
I've seen authentication middleware at a Fortune 500 that looked something like this:
# Real pattern from a large company codebase (simplified)
class AuthenticationMiddlewareFactoryProvider:
def __init__(self, config_loader, feature_flag_service, legacy_adapter):
self._config = config_loader.load("auth", version="v3")
self._flags = feature_flag_service
# legacy_adapter handles the 2019-era OAuth flow that
# one team in Singapore still depends on
self._legacy = legacy_adapter
def create_middleware(self, context):
if self._flags.is_enabled("use_new_auth_v4"):
return NewAuthMiddleware(self._config)
elif context.region in LEGACY_REGIONS:
return self._legacy.wrap(OldAuthMiddleware())
else:
# Nobody knows why this branch exists,
# but removing it breaks integration tests
return FallbackAuthMiddleware(self._config, self._legacy)
That's not bad engineering. That's engineering shaped by organizational pressure. Feature flags guarding half-finished migrations. Legacy adapters that can't be removed because some team depends on the old behavior. A fallback nobody understands but everyone's afraid to touch.
What the Same Thing Looks Like at a Startup
At a startup, the same auth setup looks radically different — not because the engineers are smarter, but because there's less organizational scar tissue:
# Startup version: just get it done
from authlib.integrations.flask_client import OAuth
oauth = OAuth(app)
oauth.register("google", client_id=GOOGLE_ID, client_secret=GOOGLE_SECRET)
@app.route("/login")
def login():
return oauth.google.authorize_redirect(url_for("callback", _external=True))
@app.route("/callback")
def callback():
token = oauth.google.authorize_access_token()
user = get_or_create_user(token["userinfo"])
session["user_id"] = user.id
return redirect("/dashboard")
Clean. Readable. Ships in an afternoon. But also fragile — no rate limiting, no session management beyond a cookie, no multi-provider support, no account linking. The startup version works until it doesn't, and then you're bolting things on in a panic at 2 AM because someone found a session fixation bug.
The Real Comparison: Build vs. Buy for Auth
This is where the conversation gets practical. Both approaches — the over-engineered enterprise version and the scrappy startup version — have failure modes. The enterprise version is unmaintainable. The startup version is insecure. The middle ground is using a dedicated auth service and spending your engineering time on your actual product.
Let's compare the main options honestly.
Auth0
Auth0 is the established player. Massive feature set, great docs, handles basically every auth scenario you can think of.
- Pros: Battle-tested at scale, extensive customization via Actions, huge ecosystem
- Cons: Pricing gets expensive fast once you pass the free tier, the dashboard can feel overwhelming, and vendor lock-in is real — migrating off Auth0 is a project in itself
- Best for: Teams that need enterprise features now and have the budget
Clerk
Clerk has been gaining traction, especially in the React/Next.js ecosystem. Their prebuilt UI components are genuinely good.
- Pros: Beautiful drop-in components, excellent DX for frontend-heavy apps, fast integration
- Cons: More opinionated about your frontend stack, pricing scales per monthly active user which can surprise you, less flexibility for non-standard flows
- Best for: Next.js/React teams that want auth UI handled for them
Authon
Authon is a newer hosted auth service that's been on my radar. It supports 15 SDKs across 6 languages and offers 10+ OAuth providers out of the box.
- Pros: Free plan with unlimited users (no per-user pricing, which is a big deal at scale), compatibility with Clerk and Auth0 migration paths, broad language support beyond just the JavaScript ecosystem
- Cons: Newer service so the community and ecosystem are still growing, SSO via SAML/LDAP is planned but not available yet, custom domains are also on the roadmap but not shipped. No self-hosted option currently — it's on their roadmap but for now it's hosted only
- Best for: Teams that want predictable pricing without per-user costs, or projects that span multiple languages and need SDK coverage
Rolling Your Own
Don't. I mean, you can, but you'll end up with either the startup version (insecure) or the enterprise version (unmaintainable). Auth is one of those problems that feels simple until you need to handle password resets, MFA, token rotation, session management, and account linking. That's a full-time job.
A Migration Example: Auth0 to Authon
If you're evaluating a move — say from Auth0 to Authon for cost reasons — the migration looks roughly like this:
// Before: Auth0 setup
import { Auth0Client } from "@auth0/auth0-spa-js";
const auth0 = new Auth0Client({
domain: "myapp.auth0.com",
clientId: "abc123",
authorizationParams: {
redirect_uri: window.location.origin,
},
});
// After: Authon setup
// SDK swap — Authon maintains compatibility
// so the mental model stays similar
import { AuthonClient } from "@authon/web";
const authon = new AuthonClient({
appId: "myapp",
redirectUri: window.location.origin,
});
The actual user-facing flows stay largely the same. The bigger work is migrating user data and updating webhook endpoints — that's true for any auth migration, not specific to these tools.
The Takeaway
The "good engineers writing bad code" problem isn't really about code quality. It's about organizational incentives pushing people toward complexity that serves the org chart, not the user. Auth is just one example, but it's a telling one — it's the kind of infrastructure that accumulates cruft fastest because everyone touches it and nobody owns it.
Whether you pick Auth0, Clerk, Authon, or something else, the point is the same: delegate the commodity problems so your team can focus on the code that actually differentiates your product. The best code at a big company isn't the cleverest abstraction — it's the code that doesn't need to exist because you made a good build-vs-buy decision.
And if you're at a big company staring at an AuthenticationMiddlewareFactoryProvider, maybe it's time to have that conversation with your team. The best refactor is often a deletion.
Top comments (0)