Starting a New Project: Which Path and Which Technologies

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Every other page in this section answers a question about changing an existing Bookshop architecture — monolith or microservices, how to migrate, and so on. This page answers the question that comes before any of that: if Bookshop did not exist yet, what would you build, and with what? It starts with the profile questions every project should answer before writing a line of code, names one default path that fits most of them, then works through seven scenario cards — each a short story, a recommended architecture, a stack drawn only from technologies already documented on this site, and what to explicitly avoid.

Profile questions

Answer these before picking an architecture or a technology. None of them has a universally right answer; each one shifts the recommendation below.

Question Why it matters

Expected size and domain complexity

A handful of screens and business rules need a fraction of the structure a large, many-entity domain needs.

Expected traffic — low, high, or bursty

Steady low traffic tolerates a single instance; high or spiky traffic needs independent, elastic scaling somewhere in the stack.

Lifetime — one-shot or long-term maintenance and evolution

A prototype handed off and forgotten does not need the structure a codebase evolved for years does.

Number of teams

One team sharing one codebase has no coordination problem for microservices to solve; several autonomous teams might.

Need for a public API, native apps, or several UIs

More than one client pushes toward a clean, versioned API layer and a separate frontend from day one, per Layering: Isolating Backend and Frontend.

Need for multi-tenancy

Serving several independent customers from shared infrastructure is a different business model, not just a technical detail — see Migration Path: From Single-Tenant to Multi-Tenant SaaS.

Compliance requirements

Data residency, auditability, or regulatory isolation requirements can force specific deployment or partitioning choices regardless of what the traffic profile alone would suggest.

Team skills and budget

The best architecture on paper is the wrong one if the team cannot operate it, or the budget cannot sustain its operational cost.

The default path

For the large majority of new projects, the default is: start as a modular monolith with a clean API layer, and add a separate frontend from day one when more than one client is expected. This is the same starting point Architectural Styles and Choosing: Monolith or Microservices? recommend for Bookshop itself: enforce module boundaries around business capabilities from the start, expose them behind one well-defined API, and resist splitting into separate deployables until a real signal justifies the operational cost.

Extract services only when one of the signals Choosing: Monolith or Microservices? names actually shows up — slowing build/test loops, capabilities that need independent scaling or release cadence, a genuine need for a different tech stack in one part of the system. Splitting earlier than that trades a monolith’s simplicity for a distributed system’s operational cost before anything justifies paying it.

The seven scenario cards below are what happens when the profile questions above pull a project away from this default — either because the default undersells the situation (a one-shot internal tool does not even need a full modular monolith) or because a real signal is already present on day one (a high-traffic consumer product, an enterprise platform, or a SaaS product change the calculus before the first commit).

flowchart TD A["New project: answer the\nprofile questions"] --> B{"One-shot delivery,\nsmall, stable?"} B -->|"Yes"| M["Monolith"] B -->|"No"| C{"More than one client,\nor a public API?"} C -->|"No"| MM["Modular monolith\n(the default path)"] C -->|"Yes"| MM MM --> D{"Multi-tenant\nSaaS product?"} D -->|"Yes"| SAAS["Control plane +\npooled application plane"] D -->|"No"| E{"A signal from Choosing:\nMonolith or Microservices?\nalready present?"} E -->|"Not yet"| STAY["Stay a modular monolith"] E -->|"Yes"| MS["Extract microservices\nfor the affected capabilities"] MS --> DEP{"Traffic shape?"} DEP -->|"Steady"| K8S["Container platform /\nKubernetes"] DEP -->|"Bursty, low baseline"| SVL["Serverless"]
A matrix with one row per project-profile scenario card — small one-shot project, startup MVP, long-lived line-of-business application, high-traffic consumer application, large multi-team enterprise platform, multi-tenant SaaS product, and batch/data-processing job — against four concern columns: architecture style, data and messaging, and deployment, summarizing each scenario’s recommendation in one picture

Seven scenario cards

(a) Small one-shot project or internal tool

A team needs a small internal tool — an admin dashboard, a one-off data-entry app for a handful of users — that will be built quickly, used by a small audience, and is unlikely to be actively developed for years.

Recommended architecture: a plain monolith. No modules, no API layer separate from the UI, no independent deployability to exercise — speed of delivery matters far more than architectural purity here.

Stack:

  • A server-rendered UI via Thymeleaf, or a small SPA with React or Vue if the UI needs meaningfully more interactivity than server rendering comfortably gives.

  • One relational database, sized and operated per Choosing the Right Database.

  • A single JAR or container, or a PaaS deployment — see Deployment below.

What to explicitly not do: no message broker (neither Kafka nor RabbitMQ earns its operational cost here), no Kubernetes — a single running instance is enough traffic headroom for an internal tool, and the orchestration overhead would dwarf the application itself.

flowchart LR U["Browser"] --> M["Monolith\n(SSR or small SPA)"] M --> D[("One relational DB")]

(b) Startup MVP that must evolve fast

A startup is validating a product idea and needs to ship an MVP quickly, while keeping the door open to pivot the product, add a mobile app, and scale traffic once something proves out — all without a rewrite.

Recommended architecture: a modular monolith behind a REST API, with a separate frontend from day one so a native app can be added later without re-architecting the backend — the same reasoning Layering: Isolating Backend and Frontend gives for splitting backend and frontend as soon as more than one client is plausible.

Stack:

  • A modular Spring Boot backend exposing a REST API (see API-First Development: REST and gRPC for a contract-first starting point).

  • React, Angular, or Vue for the web client, chosen per the team’s own familiarity — any of the three keeps a native app buildable against the same REST API later.

  • PostgreSQL as the default relational store for a domain that is still being discovered.

  • A container deployed to a managed platform (see Deployment), so the team is not also operating raw infrastructure while iterating on the product.

  • Feature flags to decouple deploying code from releasing it to users — essential once the team is shipping fast and needs to de-risk each release.

What to explicitly not do: do not extract services yet — the MVP’s domain boundaries are, by definition, not proven; per Richardson (quoted in Choosing: Monolith or Microservices?), "a startup should almost certainly begin with a monolithic application."

flowchart LR U["React / Angular / Vue SPA"] --> A["REST API\n(modular monolith)"] N["Native app (later)"] -.-> A A --> D[("PostgreSQL")] A --> F{{"Feature flags"}}

(c) Long-lived line-of-business application for one organisation

A single organisation needs an application it will run and evolve for years — an internal ERP-adjacent system, a case-management tool, an operations platform — built by a small, stable team, with real background processing and real users who need to sign in securely.

Recommended architecture: a modular monolith, with module boundaries enforced by the framework itself rather than left to convention.

Stack:

  • Spring Boot with Spring Modulith enforcing module boundaries at build time, or Quarkus if fast startup and low memory footprint matter more than the wider Spring ecosystem.

  • RabbitMQ for background work — report generation, scheduled reconciliation jobs, notification fan-out — where task-queue semantics fit better than an event log.

  • OAuth/OIDC for authentication, most likely delegated to the organisation’s existing identity provider rather than a home-grown login system.

  • Metrics, tracing, and observability wired in from day one — a long-lived application accumulates operational blind spots quickly if this is left for later.

What to explicitly not do: do not reach for microservices because the application is "important" — importance and longevity are not, by themselves, signals to split; a single autonomous team with a stable domain is exactly the profile Choosing: Monolith or Microservices? recommends keeping as one deployable.

flowchart LR U["Server-rendered UI"] --> A["Modular monolith\n(Spring Modulith / Quarkus)"] A --> Q["RabbitMQ\n(background work)"] A --> D[("Relational DB")] A --> O["OAuth / OIDC"]

(d) High-traffic consumer application

A consumer-facing product — a storefront, a media or content platform — expects high, spiky traffic against a handful of hot paths (search, catalogue browsing, checkout), while most of the rest of the system sees ordinary load.

Recommended architecture: an API gateway or backend-for-frontend at the edge, with microservices extracted only for the hot paths — not a wholesale microservices rewrite of the entire product.

Stack:

What to explicitly not do: do not turn every capability into a microservice just because the product is high-traffic — capabilities without a scaling, fault-isolation, or team-autonomy reason to split are still better off inside the monolith, per Choosing: Monolith or Microservices?.

flowchart LR U["Client apps"] --> GW["API Gateway / BFF"] GW --> S["Search\n(Elasticsearch / Solr)"] GW --> C["Catalogue cache"] GW --> OP["Order / payment sagas"] OP --> K["Kafka"]

(e) Large multi-team enterprise platform

Several autonomous teams need to build and release independently against a shared domain, each team owning a slice of the business capability, with multiple client types (web, mobile, partner integrations) consuming the platform.

Recommended architecture: microservices organized by business capability from the start, not extracted later — this is the one scenario where the operational prerequisites for microservices (Fowler’s MicroservicePrerequisites) are worth investing in up front, because the team-autonomy problem microservices solve is already present on day one.

Stack:

  • Services split by business capability, following the same decomposition reasoning Migration Path: From a Monolith to Microservices applies to Bookshop, just designed in rather than extracted later.

  • API-first contracts between services, authored with OpenAPI or Protocol Buffers before implementation starts, so teams can build against each other’s contracts in parallel.

  • A gateway plus a backend-for-frontend per client type — one BFF for the web app, one for mobile, one for partner integrations — instead of one generic API trying to serve all three well.

  • Event-driven integration between services, using Domain Events to keep services loosely coupled rather than chaining synchronous calls.

  • A database per service, per Choosing the Right Database applied independently to each service’s own data shape.

  • Distributed tracing from day one (Micrometer Tracing and OpenTelemetry) — with this many services, a request’s path through the system is unreadable without it.

What to explicitly not do: do not let "enterprise" justify skipping API-first contracts or shared observability — those are what keep several autonomous teams' services interoperable and debuggable; without them, this scenario degenerates into a distributed monolith, one of the five pitfalls Migration Overview warns every migration path about.

flowchart LR W["Web BFF"] --> GW["API Gateway"] M["Mobile BFF"] --> GW GW --> S1["Service A"] GW --> S2["Service B"] S1 --> EV["Domain events"] S2 --> EV

(f) Multi-tenant SaaS product

A product will be sold to many independent customer organisations (tenants) from shared infrastructure, with tiering, per-tenant billing, and the operational need to onboard new tenants without manual work.

Recommended architecture: control plane first, pooled application plane — the same architecture Migration Path: From Single-Tenant to Multi-Tenant SaaS describes in depth for Bookshop’s own move to SaaS. For a brand-new project, this means building onboarding, identity, and tenant management before most application features, not after.

Stack:

  • A control plane covering onboarding, identity, tenant management, billing, and metrics, built first per Migration Path: From Single-Tenant to Multi-Tenant SaaS.

  • A pooled application plane by default, with tiering (a premium siloed tier, if the business model needs one) layered on top rather than baked into the base architecture.

  • Serverless compute for the control plane specifically — its traffic is typically low and bursty (an onboarding event, an admin action) compared to the application plane’s steady load, which is exactly the profile serverless suits best; see the serverless caveat under Deployment before applying the same choice to the application plane’s JVM services.

What to explicitly not do: do not start with the application plane’s features and treat tenancy as something to add later — every guiding principle on the SaaS migration page warns against exactly this; multi-tenancy retrofitted after the fact costs far more than multi-tenancy designed in from the first service.

flowchart LR T["Tenants"] --> CP["Control plane\n(serverless)"] CP --> AP["Pooled application plane"] AP --> D[("Tiered, partitioned data")]

(g) Batch or data-processing job

A recurring job needs to process a bounded batch of data — a nightly reconciliation run, a data export, an ETL/reporting pipeline — with no interactive users and no need for a web layer at all.

Recommended architecture: a dedicated batch application with no web layer whatsoever — not a web application with a scheduled endpoint bolted on.

Stack:

  • Spring Batch for the Job/Step/chunk-oriented processing model, fault tolerance (skip/retry), and restart semantics a real batch workload needs.

  • Whatever relational or file-based source and target the job reads from and writes to, chosen per Choosing the Right Database.

What to explicitly not do: do not stand up a Spring MVC or REST layer just to trigger the job on a schedule — a batch application does not need to accept HTTP requests to run; a scheduler invoking the job process directly is simpler and has less to secure and operate.

flowchart LR SRC[("Source data")] --> J["Spring Batch job\n(no web layer)"] J --> SINK[("Target data")] SCH["Scheduler"] --> J

Cross-cutting recommendation tables

The scenario cards above combine these building blocks; the tables below make each individual choice explicit, independent of which scenario it is used in.

Backend framework

Choice When

Spring Boot

The default for most JVM backends — the widest ecosystem covered on this site (data access, messaging, security, batch, observability).

Quarkus

Fast startup and low memory footprint matter — containers that need to scale to zero, or serverless JVM deployments (see Deployment).

Java or Kotlin

Either runs on Spring Boot; Kotlin trades some ecosystem friction for null safety and less boilerplate — see the linked comparison for the full trade-off.

Python / Django

A full-stack, server-rendered web application, an admin-heavy back office or a content site, where Django’s ORM, admin, forms, auth and templates save the most work — see Django, including REST APIs with Django REST framework.

Python / FastAPI

A team already standardized on Python, or building a service that leans on Python’s data/ML ecosystem — see Python: Getting Started for the GraphQL-specific starting point, and Python FastAPI for the framework itself.

Python / Flask, Tornado or Pyramid

A Python team that wants a small, unopinionated WSGI core and picks its own components (Flask, Pyramid), or a service built around long-lived connections and non-blocking I/O (Tornado) — see Python Flask, Python Tornado and Python Pyramid.

PHP / Laravel

A PHP team or PHP hosting, building a full-stack web application or a REST/JSON:API backend with queues, scheduling and real-time features included — see PHP and Laravel.

TypeScript / NestJS

A team already standardized on TypeScript or Node.js, or sharing types and tooling with a TypeScript front end —  see NestJS for the framework, with modules, dependency injection and first-class REST, GraphQL, WebSocket and microservice support.

JavaScript / TypeScript on Node.js (built-ins, Express, Fastify)

A small service, CLI or API where Node’s built-in HTTP, test runner and SQLite are enough, or a team that prefers a minimal framework over NestJS’s structure — see Node.js for the runtime and its built-ins, and Web Frameworks: Express & Fastify.

Rust / Axum

A team that needs predictable latency, low memory and high throughput, or already writes Rust; API backends, real-time services and server-rendered htmx apps — see Axum for the framework and the Rust Reference for the language.

API style

Choice When

REST

The default for most public and internal APIs — broad tooling support and the simplest mental model for consumers.

GraphQL

Clients need to compose data from several resources in one round trip, or different clients need meaningfully different shapes of the same data — see When to Use GraphQL or a Typical REST API for the full trade-off, including its query-plan cost caveat.

gRPC

Service-to-service calls where performance and a strict contract matter more than human-readability — see API-First Development: REST and gRPC.

Messaging

Choice When

None

Synchronous calls are sufficient — most small and mid-sized applications never need a broker at all.

Kafka vs. RabbitMQ

An event backbone or a task queue is genuinely needed — see the linked comparison for which one fits: Kafka for high-throughput event streams and replay, RabbitMQ for flexible routing and traditional task-queue semantics.

Database

Choice When

Choosing the Right Database

Always the starting point — covers relational, document, search, vector, graph, key-value, wide-column, time-series, and OLAP categories, and how to keep several of them consistent once more than one is in play.

Frontend

Choice When

React / Angular / Vue

More than one client is expected (web plus native apps later), or the UI needs client-side interactivity a server-rendered page cannot comfortably give.

Vaadin / Blazor / Thymeleaf

A single team owns both backend and UI and prefers staying in one language end to end (Vaadin, Blazor) or the UI is simple enough that server-rendered templates (Thymeleaf) are the least amount of moving parts.

Jakarta Faces (JSF) / PrimeFaces

Maintaining or extending an existing Jakarta EE application, or a component-rich internal back-office UI on a Jakarta EE stack.

Bootstrap vs. Tailwind

Bootstrap for a component-driven UI that needs to look consistent quickly; Tailwind for fine-grained, utility-first control over a custom design system.

Mobile

Choice When

Kotlin / Android

A native Android app is needed and the team can invest in a platform-specific codebase.

Swift / Apple platforms

A native app is needed for iOS, iPadOS, macOS, watchOS or visionOS (SwiftUI, one multiplatform codebase across Apple devices), same trade-off as Android above.

React Native (or Flutter)

One codebase across Android and iOS is worth more than native-platform fidelity. React Native is documented on this site; Flutter is not. Evaluate against the native options above based on the team’s existing skills  — a team already fluent in React and TypeScript carries most of that knowledge across.

Authentication

Choice When

OAuth / OIDC

The default for any API or application needing delegated authorization or federated sign-in — covers every grant type and the current security best practices.

A backend-for-frontend for SPAs

A browser-based single-page app needs tokens — see Browser-Based Apps (SPAs) for why a BFF, not the browser itself, should hold the tokens.

Deployment

Stage When

Single process

A one-shot or internal tool with low, steady traffic — see (a) Small one-shot project or internal tool above.

Container

Anything beyond a single process benefits from a container’s reproducible runtime and the option to move to a managed platform later without re-packaging.

Managed container platform

A small team wants container benefits without operating the orchestration layer itself — the sweet spot for most startup MVPs ((b) Startup MVP that must evolve fast).

Kubernetes

Several services need independent scaling, rollout, and placement control — earns its operational cost once there is more than one or two services to orchestrate, per (d) High-traffic consumer application and (e) Large multi-team enterprise platform.

Serverless

Bursty, low-baseline traffic where paying only for actual invocations beats keeping an instance warm — the control plane in (f) Multi-tenant SaaS product is the clearest fit. Caveat: cold starts inflict a long-tail latency penalty on JVM services in particular; Quarkus native executables (via GraalVM/Mandrel ahead-of-time compilation) are the standard mitigation when a JVM service must run serverless, trading a longer, heavier build for near-instant startup. See also Serverless and Cloud Functions for the concrete deployment options.

For taking these deployment choices onto Google Cloud, including migration paths and multicloud trade-offs, see Architecture, Migration and Multicloud.