Architecture & Design

Technical Architecture & Design Principles

A working architect's course on making and defending system design decisions — the trade-offs, the patterns, and the judgment that separate someone who writes good code from someone who designs good systems.

10 core sections 6 interactive exercises Grounded in production fintech systems

Section 1

What Architecture Actually Is

Architecture is not a checklist of best practices you apply until the diagram looks respectable. It is a series of trade-offs, made under incomplete information, that determine how expensive change will be for the next several years.

Most engineers first encounter "architecture" as a set of rules: use dependency injection, favor composition over inheritance, keep controllers thin. Those rules are useful, but they are downstream of the actual skill. The actual skill is deciding, for this system, with this team, under these constraints, which trade-off to accept — and being able to say clearly why you accepted it and what it will cost you later.

Every non-trivial design decision buys you something and costs you something else. Splitting a service buys you independent deployment and costs you distributed transactions. Choosing a relational database buys you strong consistency and costs you horizontal write scaling. Adding an abstraction layer buys you the ability to swap an implementation and costs you a file, an interface, and a slightly longer onboarding path for new engineers. There is no trade-off-free option. Architecture is the discipline of choosing which costs you are willing to carry, deliberately, instead of discovering them by accident eighteen months in.

Some decisions are cheap. Some are not.

Not all decisions deserve the same amount of deliberation, and conflating them is how teams either move too slowly on things that don't matter or too fast on things that do.

The mark of an architect is knowing, before you write the code, which category a given decision falls into. Junior engineers often get this backwards: they agonize over naming and folder structure while nonchalantly picking a synchronous call chain across five services because "we can always change it later." You usually can't, not cheaply.

A concrete example: how invoice generation talks to the payment gateway

Take a billing platform generating a monthly invoice and needing to charge a stored payment method. There are at least three shapes this can take:

  1. The invoicing service calls the payment gateway synchronously, in-line with invoice creation, and marks the invoice paid or failed based on the immediate response.
  2. The invoicing service creates the invoice, publishes a InvoiceReady event, and a separate payment service consumes it, attempts the charge, and publishes a PaymentSucceeded or PaymentFailed event that invoicing reacts to.
  3. Invoicing calls a payment gateway abstraction that queues the charge request and polls or receives a webhook for the outcome, decoupling the invoice's lifecycle from the gateway's response time entirely.

Option 1 is the easiest to build and reason about on day one. It is also the one that is hardest to walk back. Once dozens of call sites assume invoice creation and payment capture happen in the same request, changing to an async model later means touching every caller, every test, every assumption about what "invoice created" means, and probably running a dual-write period while you migrate traffic. The team that picked option 1 for speed in month one is often still living with it in year three, not because it was the wrong call at the time, but because nobody flagged it as an expensive-to-reverse decision when it was made. Had they flagged it, they might still have picked option 1 — correctly, for a small team without yet-proven payment gateway reliability problems — but they would have done it with eyes open, and probably isolated the payment call behind an interface so that when it did need to change, the blast radius was smaller.

This is the actual value architecture adds: not predicting the future correctly, but making sure the expensive-to-reverse decisions get made consciously, with the trade-off named out loud, instead of by default because it was the first thing that compiled.

Working definition

Good architecture doesn't eliminate the cost of change — nothing does. It buys you options. A well-placed interface, a clean service boundary, an honestly-named event: these don't make future requirements free, they make future requirements possible without a rewrite. If you remember one thing from this section, remember that architecture is risk management for the cost of change, not a pursuit of elegance for its own sake.

Section 2