We Stopped Building the Same Software Twice

After building enough production systems, we noticed the same engineering problems kept coming back. Our Core Platform is how we stopped solving them from zero.

We Stopped Building the Same Software Twice

We've built a lot of different software over the years.

SaaS products. Commerce platforms. Internal tools. Operational systems. APIs. Realtime applications.

They solve different problems, for different businesses and different users.

But once you get below the product layer, you start seeing the same things.

Authentication. Organizations. Permissions. Files. Notifications. Audit logs. Search. Background jobs. Imports and exports. Integrations.

None of these are usually the reason a product exists.

But almost every serious product needs some version of them, and every one of them has to work reliably once the software reaches production.

After building these things enough times, we started asking ourselves a fairly obvious question:

Why are we solving the same engineering problems again?

That question eventually became our Core Platform.

The same features keep coming back

A new project rarely starts with a requirement to build an identity system.

It starts with:

Users need to log in.

That's simple enough.

Then users need to reset their passwords. Email addresses need to be verified. Sessions need to be managed. Accounts need to be disabled. Someone asks for two-factor authentication. An integration needs a service identity.

What started as a login form is now an identity capability.

Permissions follow the same path.

At first:

This user can edit this.

Later:

This user can edit invoices for this organization, view orders, but can't manage other users.

Now there are organizations, teams, roles, invitations and different levels of access.

Files are another good example.

Uploading a file is easy.

Running file handling in production means dealing with storage, metadata, ownership, permissions, validation, previews, references and cleanup.

Notifications start as an email.

Eventually you have:

event
  ↓
notification
  ├── in-app
  ├── email
  ├── push
  └── webhook

Then come delivery states, retries, preferences and failures.

We've seen the same pattern with search, imports, background processing, audit trails and integrations.

The products change.

A surprising amount of the engineering underneath them doesn't.

So we stopped starting from zero

Our Core Platform isn't an application that our products run inside.

There isn't a Core dashboard.

It isn't a CMS, and it isn't a starter template that generates a finished application.

It's the engineering underneath the products we build.

A growing set of capabilities, conventions and tested components that we've already built, operated and learned from in production.

Today that includes things like:

  • identity and authentication;
  • organizations and teams;
  • roles and permissions;
  • files and media;
  • notifications;
  • activity and audit trails;
  • search;
  • background processing;
  • imports and exports;
  • integrations;
  • API conventions;
  • observability.

A project uses what it needs.

Everything that actually makes that product different stays where it belongs — in the product.

That's an important distinction for us.

The Core handles shared capabilities. The product owns its business behaviour.

We don't build features for imaginary products

There's an easy mistake to make once you start building shared infrastructure.

Everything starts looking reusable.

We've learned to be careful with that.

Something doesn't become part of our Core because we think another project might need it someday.

Usually it happens the other way around.

We build something because a real project needs it.

Then another project runs into a similar problem.

By the second or third implementation, we have a much better idea of which parts belong to the business domain and which parts describe the underlying engineering problem.

We extract the useful boundary.

Then we keep learning from it in production.

That process is slower than designing a huge platform upfront.

It also produces much better abstractions.

Most of our Core exists because we've already needed it more than once.

Reusable code is the easy part

Moving some code into a shared package isn't difficult.

Keeping it useful across several products for several years is.

A shared capability has to survive database migrations, upgrades, different configurations, new security requirements and use cases that didn't exist when it was originally written.

Contracts start to matter.

Defaults matter.

Failure modes matter.

Backwards compatibility definitely matters.

A perfectly reasonable change for one application can break three others.

That changed how we think about reuse.

Code being reusable is easy. Keeping it reusable after years of production changes is the difficult part.

The value of the Core isn't that we've found a clever way to share code.

It's that a growing part of the engineering underneath a new product has already encountered real users, real data and real production constraints.

The boring features usually aren't boring

CSV import is a good example.

The first version looks something like this:

upload
  ↓
parse
  ↓
save

Then real files arrive.

Columns are missing.

Dates and numbers come in different formats.

Rows are duplicated.

Some records are valid and some aren't.

The import becomes large enough that it needs to run in the background.

Users need progress.

Failed rows need useful errors.

Someone retries the same import and shouldn't create everything twice.

Before long, the actual problem looks more like:

upload
  ↓
validate
  ↓
map
  ↓
process
  ├── success
  ├── partial failure
  └── failure
  ↓
report

Audit trails have the same story.

"Store what happened" sounds trivial until several parts of a system need to answer the same question:

Who did what, to what, and when?

Permissions, files, notifications, search and background jobs all have versions of this story.

These aren't usually the features that make it into the first product presentation.

But they're often the features that determine how well that product works two years later.

New projects start differently now

This has changed the way we approach new software projects.

A new project is still a new product.

It gets its own architecture, domain model, interfaces and business rules. We don't force different businesses into the same application just because we happen to have solved some of their underlying problems before.

But we also don't pretend every project is the first software system we've ever built.

Authentication isn't a new engineering problem for us.

Neither are the foundations for permissions, file handling, notifications, audit trails or background processing.

They still need to be designed correctly for the product.

We just aren't discovering the fundamentals from scratch every time.

That changes where we can spend our engineering time.

Instead of rebuilding infrastructure simply because a repository is new, we can get to the difficult part earlier:

understanding the business and building the software that is actually specific to it.

That's increasingly how we approach the systems we build for our clients as well as our own products.

Shared doesn't mean coupled

There's another lesson that took us longer to learn.

More shared code isn't necessarily better.

If moving something into the Core means several otherwise independent products become tightly coupled, we've probably made the architecture worse.

The useful question isn't:

Can we reuse this?

It's:

Does this responsibility actually belong here?

Sometimes the right answer is no.

Sometimes a small amount of duplicated code is cheaper than the wrong abstraction.

And sometimes something that looked product-specific turns out, after the third implementation, to be a capability we've understood well enough to share.

The goal isn't maximum reuse.

The goal is to reuse the things whose boundaries we actually understand.

This also changed how we work with AI

More recently, this architecture has become useful in a way we didn't originally design it for.

Coding agents work much better when responsibilities are explicit.

Being able to say:

Core       → identity, permissions, shared capabilities
Messaging  → conversations and delivery
Billing    → subscriptions and payments
App        → product behaviour and presentation

is useful to a developer.

It's also useful to an agent.

The model still has to understand the task and the code. But it doesn't have to infer every architectural decision from whichever files happen to be in its context.

This is one of the reasons our Core Platform and CS AI Core increasingly complement each other.

One gives our projects established engineering capabilities.

The other helps make the architecture, constraints and decisions around those projects explicit enough for people and coding agents to work with them safely.

Neither replaces engineering decisions.

They help us preserve them.

Production is where the Core gets better

Our Core Platform isn't finished.

We don't really expect it to be.

Every production system exposes another assumption.

Sometimes we discover an edge case in an existing capability.

Sometimes we find a boundary that isn't as clean as we thought.

Sometimes we solve a problem for the third time and realize it shouldn't be solved a fourth.

And occasionally we discover that something we tried to generalize should never have left the product in the first place.

That's part of maintaining shared software.

The Core isn't a separate product sitting next to the work we do.

It is a result of that work.

It carries forward things we've learned building SaaS products, commerce systems, realtime applications and operational software — including the mistakes and production problems that aren't visible in a feature list.

And the next system benefits from those lessons.

We don't start from zero anymore

That's ultimately what our Core Platform is for.

Not to make every product look the same.

Not to force every project into the same architecture.

And not to build a giant framework that tries to predict every requirement we'll ever encounter.

It's the accumulated engineering work behind the software we build: capabilities we've implemented before, problems we've already encountered, and components we no longer have a good reason to reinvent.

Every product is still its own product.

Every business still has its own problems.

We just don't start from zero anymore.