Architecture Decisions
ADR-0003: Shared Package Strategy
Decision to organize shared code into domain-focused packages with clear boundaries
Status
Accepted
Date: 2025-02-03
Context
In a monorepo with multiple apps (CalNexus, Lexilink, Qript), we need a clear strategy for sharing code. Without careful design, we risk:
- Tight coupling: Changes in one app breaking another
- Circular dependencies: Packages depending on each other in complex ways
- Bloated packages: “Kitchen sink” packages that try to do everything
- Unclear ownership: No one knows who maintains what
- Inconsistent patterns: Each app reinventing the same logic
We needed to define:
- What belongs in a shared package vs. app-specific code
- How packages depend on each other
- Naming conventions and structure
- How to version and evolve packages
Decision
We organized shared code into domain-focused packages with clear boundaries:
Package Categories
-
Core Infrastructure (stable, rarely changes)
@hn-monorepo/ui— shadcn/ui components + design system@hn-monorepo/config-typescript— Shared TS config@hn-monorepo/config-biome— Linting/formatting config
-
Business Logic (evolves with products)
@hn-monorepo/auth— Authentication wrappers (Convex Auth)@hn-monorepo/billing— Stripe integration and subscription logic@hn-monorepo/storage— File upload/download helpers@hn-monorepo/analytics— Event tracking abstractions
-
Integrations (external APIs)
@hn-monorepo/email— Email templates and nodemailer@hn-monorepo/ai— AI SDK wrappers (Anthropic, OpenAI)@hn-monorepo/i18n— next-intl configuration
-
Development (tooling)
@hn-monorepo/testing— Shared Vitest/Playwright setup@hn-monorepo/monitoring— OTel configuration
Package Rules
- Packages depend on packages, never on apps
- Packages are small and focused — each has a single, clear purpose
- Packages export only what is needed — use
index.tsto control public API - Packages are tree-shakeable — export individual functions/components, avoid side effects
- Apps can do whatever they want — app-specific logic stays in the app
Example Package Structure
// packages/billing/
├── src/
│ ├── index.ts // Public API
│ ├── stripe.ts // Stripe client setup
│ ├── webhooks.ts // Webhook handlers
│ └── subscriptions.ts // Subscription logic
├── package.json
└── tsconfig.json
// apps/calnexus/app/api/webhooks/stripe/route.ts
import { handleStripeWebhook } from "@hn-monorepo/billing"
export async function POST(req: Request) {
return handleStripeWebhook(req)
}
Consequences
Positive
- Clear boundaries: Easy to know where code belongs
- Reusability: Write once, use in all apps
- Type safety: Packages enforce contracts between apps
- Independent evolution: Can update packages without touching apps (usually)
- Easier testing: Test packages in isolation
- Reduced duplication: No more copy-paste between projects
Negative
- Abstraction overhead: Sometimes packages add indirection
- Breaking changes are painful: Changing a package API affects all apps
- Over-engineering risk: May create packages prematurely
- Build complexity: Changes to packages require rebuilding dependent apps
Neutral
- Requires discipline: Team must follow the rules consistently
- Versioning strategy: Currently using
workspace:*, may need semantic versioning later
Alternatives Considered
Alternative 1: No Shared Packages (Duplication)
- Description: Each app owns all its code, copy-paste when needed
- Pros: Complete independence; no coordination overhead
- Cons: Massive duplication; bugs fixed in one app but not others; inconsistent UX
- Why not chosen: Defeats the purpose of a monorepo
Alternative 2: Single “Kitchen Sink” Package
- Description: One
@hn-monorepo/sharedwith everything - Pros: Simple dependency graph; one place to look for shared code
- Cons: Bloated, hard to navigate; unclear ownership; bundle size issues
- Why not chosen: Does not scale as codebase grows
Alternative 3: Feature-Based Packages
- Description: Packages organized by feature (e.g.,
@hn-monorepo/calendar) - Pros: Aligned with product domains; could be extracted as standalone products
- Cons: Unclear where cross-cutting concerns go; risk of tight coupling to specific apps
- Why not chosen: Our products are too different; domain packages work better
References
Notes
- We use
workspace:*for internal package dependencies (managed by Bun) - Packages should remain framework-agnostic when possible
- When in doubt, keep code in the app until you need it in a second place (YAGNI principle)