ArcOS
← Case Studies
WebArchitectureScalabilityGreenfield

Built to Scale: A Greenfield Platform Designed for Growth

How a greenfield web platform was architected from day one to grow from a single deployable system into as many independent, cooperating services as the business required.

September 2025

Starting from zero is a rare opportunity. Most systems inherit constraints from decisions made years ago by people who no longer work there. A greenfield project is a chance to make good decisions first — but only if you treat it as one.

The situation

The client was building a new product from scratch. There was no existing system to integrate with, no legacy codebase to work around, and no inherited technical debt. There was also no existing user base, no proven load profile, and no certainty about which parts of the product would grow fastest.

The product was a web platform with multiple distinct domains: user management, content, transactional flows, notifications, and an administration layer for internal operations. Each domain had its own data model, its own rules, and its own performance characteristics. Some would be read-heavy; others would be write-heavy. Some would need to scale dramatically as the user base grew; others would stay small regardless.

The architectural question was not "how do we build this?" but "how do we build this so that the decisions we make today don't become the constraints that slow us down in three years?"


Starting as one, designed to become many

The first decision was the most important: start with a single deployable system, but design every internal boundary as if it were eventually going to be a separate one.

The alternative — starting with many separate services immediately — is a common mistake in greenfield projects. It multiplies the operational complexity of the system before there is enough load to justify it, before the team understands the domain well enough to draw the right boundaries, and before there is enough traffic to know where the performance bottlenecks actually are.

A system that is deployed as one but structured as many gets the best of both: the simplicity of a single deployment in the early stages, and the option to extract services later without rewriting the application.

Defining boundaries before writing code

Before a line of code was written, the team spent two weeks mapping the domain. Each area of the product — user accounts, content management, transactional processing, notifications, administration — was defined as a bounded context: a named area of the system with its own language, its own data ownership, and explicit interfaces to everything outside it.

These boundaries were not technical constructs. They were business constructs. The user account domain owned everything about a user's identity and preferences. It did not own their transaction history — that belonged to the transactional domain. It did not own the content they had created — that belonged to the content domain. The user domain knew these things existed, but it referred to them by identifier rather than embedding their data.

This discipline felt bureaucratic in week two. By month six, when the team needed to extract the notification system into its own independently deployable service, the boundary already existed. The extraction took three days.


How the system was structured

A shared communication contract

Each domain exposed a well-defined interface — a set of operations it would perform and events it would emit when something changed. Other domains could call operations and subscribe to events. No domain reached into another domain's data store directly.

This was enforced structurally, not through convention. The build system prevented direct database access across domain boundaries. If the content domain needed user information, it requested it through the user domain's interface. This meant the user domain could change its internal implementation — switch storage engines, restructure its tables, add caching — without any other domain needing to know.

Events were the primary communication mechanism for things that had happened. When a user completed a transaction, the transactional domain emitted an event. The notification domain subscribed to that event and sent a confirmation. The content domain subscribed to it and updated the user's activity record. Neither domain needed to know about the other — they both just cared about the event.

The configuration layer

One of the early architectural decisions with the largest long-term impact was treating configuration as a first-class concern from day one.

Every domain had a configuration surface — a set of values that controlled its behaviour and could be changed without a deployment. Feature flags, rate limits, content rules, pricing parameters, and operational thresholds were all configurable at runtime.

This served two purposes. In development, it made it easy to test different behaviours without code changes. In production, it gave the operations team the ability to respond to situations — a spike in traffic, a pricing change, a feature rollout — without waiting for a deployment cycle.

The configuration system itself was one of the first candidates for extraction into a standalone service, precisely because every other domain depended on it and it needed to be available and fast regardless of what else was happening in the system.


Growing the system

The first extraction

Four months after launch, the notification domain was extracted into its own deployable service. The reasons were practical: notification volume was growing faster than expected, the processing was CPU-intensive, and scaling the entire system to handle notification load was wasteful.

Because the boundaries had been defined correctly, the extraction was mechanical. The interface the notification domain had been exposing internally became an external API. The event subscriptions that had been in-process became network calls. The data the notification domain owned moved to its own store. The rest of the system did not change — it was already communicating with the notification domain through its interface, and that interface stayed the same.

Extraction took three days of development and two days of testing and staged rollout. There were no incidents.

The second extraction

Six months later, the transactional domain was extracted for similar reasons — and because it carried different compliance requirements from the rest of the system. Isolating it meant the compliance boundary was architectural rather than procedural. Auditing, access controls, and data handling rules applied to a clearly defined service rather than to a portion of a larger system.

This extraction was slightly more complex because the transactional domain had more event subscribers, but the principle was the same. The interface existed. The extraction was mechanical.

What stayed together

Not every domain was extracted. User management, content, and administration remained in the main deployable system throughout the period covered by this case study. The load profile didn't justify extraction, the operational overhead of three additional services wasn't warranted, and the team's capacity was better spent on product development than infrastructure management.

The option to extract existed if the need arose. It never did. That is the point of designing for optionality rather than prematurely optimising for a scale that hasn't arrived.


CI as the backbone of confidence

A system designed to grow is a system that changes frequently. The CI pipeline was the mechanism that made frequent change safe.

Every domain had its own test suite — unit tests, integration tests against a local version of the full system, and contract tests that verified each domain's interface matched what the other domains expected of it.

Contract testing was the critical addition that wouldn't have existed in a single-domain system. When the notification domain declared that it expected user events to have a certain shape, and the user domain declared that it would emit events in that shape, the CI pipeline verified that both declarations were true and consistent. A change to the user event format that broke the notification domain's expectation failed the build before it reached code review.

This meant that the team could change any domain with confidence that they hadn't broken a contract they weren't aware of. As the system grew and the number of inter-domain interactions increased, this became more valuable, not less.

Every merge to the main branch required:

  • Passing unit tests for the changed domain
  • Passing integration tests for the full system in a local environment
  • Passing contract tests for every interface the changed domain participated in
  • A passing build and deployment to the staging environment

The staging environment was a complete replica of production, including all extracted services. No change reached production without having been verified in a system that behaved identically.


Outcomes

Eighteen months after the initial launch:

  • The platform ran as three independent deployable services — the core application, the notification service, and the transactional service — plus the configuration service that all three depended on. Two more extractions were planned as volume continued to grow.
  • Every extraction was completed without a production incident. The boundary-first design made extractions mechanical exercises rather than architectural reworks.
  • The team deployed to production an average of twelve times per week. The CI pipeline and staging environment made this safe. Deployment was not an event — it was a routine activity.
  • Contract tests caught four breaking interface changes before they reached staging. In a system without contract testing, at least two of these would have reached production.
  • No domain had ever directly accessed another domain's data store. The structural enforcement had been absolute. The system's internal architecture at eighteen months looked, in terms of boundaries and interfaces, exactly like the map drawn before the first line of code was written.

What greenfield projects get wrong

The most common mistake in greenfield projects is not bad technical decisions — it is the absence of boundaries. When there are no constraints on where code can go or what can call what, entropy fills the space. A system that starts without boundaries quickly becomes a system where everything depends on everything, and extracting any part of it becomes a months-long project.

The second most common mistake is premature extraction — separating things into independent services before the domain is understood well enough to know where the boundaries actually are. A boundary drawn in the wrong place is harder to fix than no boundary at all, because it becomes load-bearing before anyone realises it is wrong.

The approach that works: start as one, draw the boundaries as business boundaries rather than technical ones, enforce them structurally, and extract when the operational need is clear. The architecture at any point in time should reflect the current operational reality of the system — not an aspiration, and not a legacy.

Let's build something that lasts.

Tell us about your product and we'll be straightforward about what it takes.