Architecture Patterns

Project: Choose an Architecture

18 min Lesson 10 of 10

Project: Choose an Architecture

Throughout this tutorial you have studied nine distinct architectural patterns — from the classic monolith, through API gateways and service meshes, CQRS and event sourcing, all the way to serverless functions. The capstone challenge is to apply that knowledge as a practitioner would: given a concrete system brief, select the right combination of patterns, justify every choice with real trade-offs, and produce a defensible design.

This lesson walks you through a worked example — an imaginary but realistic product — so you can see the decision-making process from start to finish. Use it as a template for your own architecture proposals.

The System Brief

You have been asked to design the backend for FleetTrack, a logistics platform with the following requirements:

  • Real-time GPS tracking — 50,000 vehicles each reporting their location every 10 seconds (~5,000 location writes/second at peak).
  • Driver mobile app — REST API for job assignments, documents, and turn-by-turn instructions.
  • Dispatcher web dashboard — live map with vehicle positions, route optimization, and alerts.
  • Analytics and reporting — historical route replays, fuel consumption reports, SLA dashboards. Queries can scan months of data.
  • Notification engine — push notifications and SMS for geofence events (vehicle enters or exits a zone).
  • Team size: 12 engineers today, growing to ~35 over two years.
  • Maturity: The company has basic AWS infrastructure but limited Kubernetes experience.
Before choosing patterns, characterize your workloads. FleetTrack has three very different load profiles: a write-heavy, latency-sensitive ingestion stream (GPS events), a read-heavy, low-latency query path (live map), and a scan-heavy, batch analytics path. No single database or architectural pattern serves all three equally well. Identifying this upfront is the most valuable step you can take.

Step 1 — Identify Domain Boundaries

Domain-Driven Design asks: where does the ubiquitous language change? In FleetTrack the boundaries are clear:

  1. Ingestion Domain — receives raw GPS frames from vehicles.
  2. Tracking Domain — maintains current vehicle state (position, speed, heading).
  3. Dispatch Domain — manages jobs, drivers, and assignments.
  4. Analytics Domain — historical queries and aggregated reports.
  5. Notification Domain — geofence evaluation and message dispatch.

These boundaries map naturally to services. But with 12 engineers you do not want 10 independently deployed services on day one. The right move is a modular monolith for Dispatch and Notifications (they share relational data and change together) and independent services only where the workload profile genuinely differs.

Step 2 — Apply CQRS Where Reads and Writes Diverge

The GPS ingestion path writes at 5,000 events/second. The dispatcher dashboard reads the current position of thousands of vehicles simultaneously. These two needs are diametrically opposed: the write side needs append throughput; the read side needs sub-100ms point lookups.

This is a textbook case for CQRS. The command side appends raw GPS frames to an event log (Kafka topic or Kinesis stream). A projection consumer reads that stream and maintains a current_positions table in Redis, with each vehicle's latest state keyed by vehicle ID. The query side reads only Redis — it never touches the raw event log at query time. This decouples write throughput from read latency completely.

The Analytics domain consumes the same Kafka topic asynchronously through a second consumer that writes to a columnar store (Amazon Redshift or ClickHouse) for long-range scans. The write path is unaffected by slow analytical queries.

FleetTrack CQRS and event-sourcing pipeline for GPS data Vehicles 50k GPS/10s API Gateway TLS termination rate-limit Ingest Service validate + enrich publish event Event Stream Kafka / Kinesis gps.raw topic projection Redis current positions analytics ClickHouse historical scans Live Dashboard WebSocket / SSE Reports UI async queries CQRS: command vs query stores Geofence Svc evaluate + notify push / SMS
FleetTrack ingestion pipeline: CQRS separates the 5k writes/s command path from the low-latency Redis read model and the asynchronous analytics projection.

Step 3 — Choose an API Pattern for the Client-Facing Layer

The driver mobile app and the dispatcher web dashboard have completely different data needs. The mobile app wants a single /me/job endpoint that returns the job, the assigned vehicle, and a condensed driver profile in one call. The dashboard wants a stream of real-time position updates for potentially thousands of vehicles at once.

This is exactly the problem Backends for Frontends (BFF) was designed to solve. Two thin BFF services sit behind the API Gateway:

  • Mobile BFF — aggregates Job, Driver, and Vehicle data into device-optimized payloads. Handles offline sync concerns. GraphQL or REST.
  • Dashboard BFF — maintains a WebSocket connection per dispatcher; pushes Redis position updates as Server-Sent Events. Manages per-user geofence subscriptions.
BFF services should be thin orchestrators, not business logic owners. If you find yourself writing pricing calculations or geofence evaluation inside a BFF, that logic belongs in the domain service. BFFs assemble and adapt data; they do not own it.

Step 4 — Decide the Monolith vs Service Boundary

Given a team of 12 engineers and limited Kubernetes experience, deploying every domain as an independent service is reckless. The recommendation for FleetTrack at launch:

  • Independent services (justified by workload divergence): Ingest Service, Geofence Service, both BFFs.
  • Modular monolith (one deployable unit, cleanly separated internal modules): Dispatch module + Driver Profiles module + Notification template management.
  • Managed infrastructure: Kafka (MSK), Redis (ElastiCache), ClickHouse (or Redshift). Not self-hosted.

As the team grows to 35 engineers, the Dispatch module becomes a candidate for extraction — its team will have grown large enough and its domain boundary is already clean inside the monolith, making a future Strangler Fig extraction low-risk.

Step 5 — Event Sourcing for the Audit Trail

GPS data is inherently event-sourced: each location ping is an immutable fact that happened at a point in time. Storing it as such (in Kafka with a long retention window, or in an immutable S3 Parquet lake) gives you route replay, dispute resolution, and regulatory compliance for free. You do not reconstruct state by mutating rows — you replay the event log from any timestamp.

The Dispatch module does not need full event sourcing. Job assignments change infrequently and the team does not yet need a full audit log there. Over-applying event sourcing adds operational overhead without proportional benefit. Apply it where immutability of history has clear business value.

Step 6 — Where Serverless Helps

Several workloads in FleetTrack are excellent serverless candidates because they are spiky, short-lived, and infrequent:

  • Report generation — a dispatcher requests a fuel summary for 200 vehicles over 90 days. This is a heavy scan that runs occasionally. A Lambda / Cloud Run function triggered from a queue spins up, queries ClickHouse, generates a PDF, and exits. No server to keep warm for this rare workload.
  • Geofence zone import — a GeoJSON file with 500 zones is uploaded. A serverless function parses and upserts the zones. Runs once per upload, completely event-driven.
  • Scheduled compliance exports — a nightly cron Lambda exports driving-hours data to a regulatory SFTP server.
Do not route the real-time GPS ingest path through serverless. At 5,000 writes/second, the cold-start latency and per-invocation overhead of functions makes them a poor fit. Functions are the wrong tool for sustained, high-frequency, latency-sensitive workloads. Keep that path on a long-running Ingest Service with a persistent Kafka producer connection.

The Final Architecture at a Glance

FleetTrack final architecture overview combining multiple patterns Mobile App Driver Dashboard Dispatcher GPS Devices 50k vehicles API Gateway auth, rate-limit, routing Mobile BFF REST aggregator Dashboard BFF WebSocket / SSE Ingest Service 5k writes/s Modular Monolith Dispatch | Profiles Notifications mgmt Event Stream Kafka (MSK) gps.raw topic Geofence Svc push / SMS alerts Redis current positions ClickHouse analytics Serverless Fns reports, exports ClickHouse (shared read)
FleetTrack combined architecture: API Gateway, BFF pattern, CQRS event pipeline, modular monolith for Dispatch, and serverless functions for bursty workloads.

Justification Summary

Every pattern chosen has a specific, measurable reason:

  • API Gateway — single TLS termination point, rate-limits the GPS flood from 50,000 devices, handles authentication centrally.
  • BFF (Mobile + Dashboard) — driver app needs low-bandwidth, aggregated REST; dashboard needs a WebSocket stream. One generic API cannot serve both optimally.
  • CQRS + Event Sourcing on GPS data — write throughput (5k/s) is incompatible with analytical queries (full-table scans over months). Separating the write model (Kafka) from the read models (Redis, ClickHouse) makes each optimal for its use case.
  • Modular Monolith for Dispatch — team is 12 engineers with limited Kubernetes experience. Dispatch and Profiles change together frequently. A distributed service boundary here would add network latency and deployment overhead with no scalability benefit at this load level.
  • Serverless for reports and exports — bursty, infrequent, scan-heavy. No persistent server needed. Cost-effective.
  • Strangler Fig exit plan — the modular monolith is structured so the Dispatch module can be extracted when the team grows, without a rewrite.
The best architecture is the one your team can operate. A technically perfect design that requires skills your team does not have will fail in production. FleetTrack could theoretically run on a full Kubernetes service mesh with Istio, but with 12 engineers and no Kubernetes expertise, that would be a liability. Architecture must be calibrated to organizational readiness, not just to textbook ideals.

Your Turn

Apply this same methodology to a system of your choosing. Start with the brief: what are the workloads and their distinct characteristics? Map them to domain boundaries. Identify which patterns serve each workload. Explicitly state what you are not doing and why. Document the exit plan — how does this architecture evolve as load or team size grows?

A well-reasoned design with clear trade-offs written on one page is worth more than a diagram full of buzzwords. The goal is to communicate confidence to your team, your stakeholders, and your future self.

Tutorial Complete!

Congratulations! You have completed all lessons in this tutorial.