Back to Blog
ArchitectureEnterpriseLeadership

5 Years Building Enterprise Software: Lessons Learned

AJ
Aaron J. Spurlock
8 min read

After nearly five years of building Evergreen - a comprehensive enterprise platform that handles CRM, business intelligence, call center operations, and client portals—I've accumulated a wealth of lessons that I wish I'd known from day one.

1. Start with the Data Model

The single most important decision you'll make is your data model. Everything else - UI, features, integrations—can be changed relatively easily. Your core data model cannot.

Spend the time upfront to really understand:

  • What are the core entities in your domain?
  • How do they relate to each other?
  • What data will you need for reporting?
  • How might requirements evolve?
// Early mistake: Tightly coupled address to customer
interface Customer {
  name: string
  address: string  // 🚫 What about multiple addresses?
  city: string
  state: string
}

// Better: Flexible, normalized structure
interface Customer {
  id: string
  name: string
  addresses: Address[]  // ✅ Supports multiple, typed addresses
  primaryAddressId: string
}

interface Address {
  id: string
  type: 'billing' | 'service' | 'mailing'
  street: string
  city: string
  state: string
  zip: string
}

2. Boring Foundation, Strategic Innovation

When I started Evergreen, I was tempted to use cutting-edge patterns everywhere. The key lesson: keep your foundation boring, but don't be afraid to use advanced patterns where they genuinely solve problems.

Our core stack is deliberately boring:

  • PostgreSQL for everything database-related (with read replicas for BI)
  • Redis for caching and real-time features
  • Vue.js/Nuxt for the frontend
  • Nest.js for the backend

But we strategically adopted advanced patterns where they made sense:

  • Event Sourcing for audit trails and complex workflows where we need complete history
  • Microservices for specific high-scale features that benefit from isolation
  • Queue-based Processing with BullMQ for background jobs and async operations

The lesson: Choose boring technology for your foundation, but don't dogmatically avoid modern patterns when they solve real problems. The key is being intentional about when to use what.

3. Invest in Developer Experience

Every hour spent improving developer experience pays dividends for years. Some investments that have paid off massively:

Type Safety Everywhere

TypeScript on both frontend and backend with shared types:

// shared/types/lead.ts
export interface Lead {
  id: string
  firstName: string
  lastName: string
  email: string
  phone: string
  source: LeadSource
  score: number
  createdAt: Date
}

// Used in both frontend and backend
// Compile-time errors when contract changes

Automated Testing

Not 100% coverage - that's diminishing returns. But comprehensive tests for:

  • Critical business logic
  • API contracts
  • Complex UI interactions

Local Development

One command to spin up the entire stack locally:

docker-compose up
# Database, Redis, API, and frontend all running

4. Features Are Forever

Every feature you add becomes a feature you maintain. Forever.

Before adding anything, I now ask:

  • Who specifically needs this? "Everyone" is not an answer.
  • What problem does it solve? If you can't articulate it, don't build it.
  • What's the maintenance burden? Features have ongoing costs.
  • Can we solve this with existing features? Often yes.

The best code is code you don't write. The best feature is one you don't build.

5. Communication > Code

As the team grew, I realized that communication became more important than any technical decision.

What helps:

  • Architecture Decision Records (ADRs): Document why you made choices, not just what you built
  • Runbooks: Step-by-step guides for common operations
  • Incident postmortems: Learn from failures without blame
  • Regular demos: Show don't tell

Good documentation is a feature. It reduces bus factor, speeds up onboarding, and prevents repeated mistakes.

Looking Ahead

Five years in, Evergreen is more successful than I ever imagined. It's grown from a simple CRM to the nervous system of the entire business.

The next five years will bring new challenges—AI integration, mobile expansion, and scaling the team. But the foundational lessons remain the same:

  • Invest in foundations
  • Choose simplicity over cleverness
  • Communicate relentlessly
  • Build for maintainability

The best enterprise software isn't the most technically impressive; it's the software that reliably solves real problems, year after year.

Written by Aaron J. Spurlock

Head of Technology at Midtown Home Improvements. Building enterprise-grade software and writing about the journey.