Skip to main content

22 — The map and the gaps

Read this first: this is the closing chapter, and it is the least flattering one. The first half is the map — the one shape twenty-one lessons taught you, measured against the tree they were read from. The second half is the honest list of what this course never opened, each with the document that owns it, so that a gap is a forwarding address rather than a hole.

Time: about 15 minutes. Assumes lesson 21.

The map: one direction, five files​

The course README made you one testable promise: by the end you can open any controller in backend/src/main/java/com/motorph/payroll/, trace its request down to the rows it touches, and know which file to copy when you build your own. That promise is really a shape, and the shape does not vary:

XController @RestController, @PreAuthorize, DTO in and DTO out — no logic
XService the interface: the vocabulary, no Spring, no SQL
XServiceImpl @Service, @Transactional — the logic, and its only home
XRepository Spring Data; XSpecification when the filters are dynamic
X the @Entity, and the table a migration built for it

The one-glance test. Open a file in controller/ and read three things. The class annotation tells you it is on the paved road. The return type tells you whether the boundary is honest — a ...Dto or a Page<...Dto>, never an entity (lesson 09). And what the method delegates to tells you where to go next: a field typed as an interface whose implementation is the same name plus Impl, one package down in service/impl/. If a filter is involved, the predicates are in repository/specification/ and both query paths share them (lesson 07). The mapper is the file you never see in a diff, because MapStruct generates it at compile time and the house config makes an unmapped field an error.

The counts are real. Run them yourself from the repo root:

cd backend/src/main/java/com/motorph/payroll
grep -rlE '@RestController' controller | wc -l # 85
ls service/*.java | wc -l # 81
ls service/impl/*.java | wc -l # 97
ls repository/*.java | wc -l # 89
ls repository/specification/*.java | wc -l # 43
grep -lE '@Entity($|[^A-Za-z])' model/*.java | wc -l # 92
grep -lE '@Mapper($|[^A-Za-z])' mapper/*.java | wc -l # 21
ls ../../../../resources/db/migration | wc -l # 107

Eighty-five @RestController classes in a directory holding eighty-six files — the one that is not a REST controller is TypingController, the STOMP endpoint from lesson 14. Ninety-seven implementations against eighty-one interfaces, because pure helpers like the daily-pay calculator live beside the services without one (lesson 17). Ninety-two entities, twenty-one mappers, and 107 migrations numbered V1 through V108, with one hole at V26. Against 942 main sources in total: this course read a few dozen of them closely and taught you the rule for the rest.

Billing: a port, an adapter, and a stub that must not exist twice​

Subscription billing is written as ports and adapters. PaymentProvider is the port; PolarPaymentProvider is today's adapter; StubPaymentProvider is an offline second implementation that lets the whole checkout-to-active flow run with no network and keeps CI green with no secrets. Billing owns this area.

Take one parting specimen, because it is the same reasoning you have been reading since lesson 10. Webhook intake is unauthenticated by necessity — providers carry no JWT — so authenticity comes from a per-provider signature check. The stub verifies no signatures at all. Its class comment states the consequence plainly:

* <strong>It verifies no signatures.</strong> Since {@code /api/webhooks/**} is public, a
* stub bean in production would let anyone forge an activation. The conditional below is
* what prevents that: outside {@code billing.provider=stub} this bean does not exist, so
* {@code /api/webhooks/stub} resolves to nothing and 404s.

Predict: production runs the real provider, and someone posts a hand-written activation event to /api/webhooks/stub. What is the response?

The route resolves — the path variable is {provider}, so Spring matches it happily. What fails is the lookup: with no stub bean registered, PaymentProviderRegistry.get throws ResourceNotFoundException, which lesson 05 already taught you comes back as a 404 in the one error shape. A @ConditionalOnProperty is doing security work here, and a test pins the reasoning so nobody relaxes it.

The other specimen worth naming without repeating: the Polar webhook key derivation is a trap. The configured secret is used as the HMAC key in a way that contradicts the intuitive reading of the Standard Webhooks spec, a stock library gets it backwards, and the failure mode is a silent signature mismatch on every delivery rather than an error you can grep for. The verifier's class comment argues the whole thing, and its unit test pins both behaviours. Read them before you touch that file.

Signup: two status codes, two ceilings​

A company can create its own workspace with no operator involved, and the flow is deliberately two steps. The first request answers 202, not 201, because nothing has been created — a code is now pending. The second request answers 201 with a token pair, because by then a tenant, an admin user, and a seeded workspace exist. Self-serve signup owns the flow, the trial lifecycle, and the guard rails.

The memorable line is about the rate limits. There is a per-IP limiter in front of the endpoint — you met that machinery in lesson 11 — and a separate daily cap counted against the email address, which exists because those two defend against different people. The comment in SignupVerificationService says why in one clause:

// The per-address ceiling. Counted against the address rather than the caller because the
// caller is whoever is cheapest to become -- IPs rotate, the victim's inbox does not.

The per-IP limit protects the service. The per-address cap protects a stranger who never asked to be signed up, and it is the reason the two cannot be collapsed into one.

The portal and the operator surface​

Two whole surfaces got pointers and no chapters. The customer portal — catalog, cart, orders, profile, and its own authentication — is five controllers whose rules this course never read. The platform-operator surface is the tenant registry (controller/admin/TenantAdminController), which lesson 13 touched from the tenancy side and left there.

One property of the operator surface is worth restating because it is the kind of thing people assume: there is deliberately no impersonation. The Super Admin role holds exactly three platform permissions and nothing from the tenant domain — the migration that creates it says so in its header, and describes the role as holding no tenant-domain permissions at all. Operating the platform and reading a tenant's payroll are separate powers, and the second is not implied by the first. If you go looking for a "log in as this tenant" button, the absence is the design.

HR analytics: a read path that never loads an entity​

Everything Part 2 taught about the request vertical has one significant exception in this codebase, and the analytics read path is it. HrAnalyticsServiceImpl holds a JdbcTemplate, not a repository, and reads from a dozen vw_hr_* database views created in V65__hr_analytics.sql and extended later. No entities, no specifications, no mapper — SQL text and a row mapper. That is why lesson 13 cares so much about statements Hibernate did not generate: this is the read path the database policies have to catch on their own.

The client half of this feature was decoded, in the other course: Frontend 101 lesson 18 reads the drilldown drawer that consumes these endpoints. Between that lesson and this paragraph you have both ends and none of the middle.

Recruitment, CRM, inventory: same shape, different nouns​

Seven CRM controllers, eight recruitment ones, nine across inventory and the storefront: two dozen modules this course never named. They are not missing because they are hard. They are missing because reading them would have taught you nothing that lessons 06–09 did not — the same controller-to-specification vertical, the same paged DTO envelope, the same mapper rules, with CrmDeal or JobApplicant where Employee used to be.

That is the whole argument for a convention. A codebase where every module is interesting is a codebase you have to read in full. Open CrmDealController and check the shape against the map above; if it matches, you already know how it ends.

Government forms and the depth of EWT​

Part 4 taught the engine's architecture and stopped exactly where the statutory detail began. The form generators and the expanded-withholding module are real code with real depth — filings, form layouts, and per-payee tax treatment — and none of it is decoded here. Business rules owns the rates, the premium-pay matrix, the withholding sections, and the EWT rules; the compliance references own the form layouts themselves.

Monitoring and deployment​

How this backend is built, shipped, watched, and rolled back is a different subject with its own material: DevOps for reference, and Learn DevOps if you want the pipeline taught rather than described. Nothing in this course tells you what to do at 2 a.m.; that course does.

Part 4 was a choice, and it is also an instruction​

The README stated this as a limit. State it once more as a positive rule, because it is how you should extend this course:

Architecture belongs here; numbers belong in the rules document; algorithms belong in the source. Part 4 taught you the frozen-payslip spine, the lifecycle gates and the snapshot rule, the pure computational core, cohort versioning with effective dates, and the true-up concept — because those are load-bearing ideas that a reader cannot recover from a table of rates. It did not restate a single contribution bracket, because the brackets have one home in business-rules.md, and a copy in a lesson is a copy that goes wrong silently the year the schedule changes.

When you add a lesson, hold that line. If your draft contains a number a regulator could change, it belongs in the rules document with a link from your prose.

The code moves; this course does not​

Every count in this chapter was measured against the working tree on the day it was written, and the tree has not stopped moving. You can watch it happen without leaving the docs: the package table in the Backend reference reports seventy-eight controller classes, eighty-five entities, eighty-two repositories, thirty-nine specifications and twenty mappers, where today's tree has eighty-six, ninety-two, eighty-nine, forty-three and twenty-one. Backend tests counts 919 main sources where the tree now has 942 — though that page does the honest thing and dates its measurement, telling you to regenerate rather than trust it.

Those numbers are not fixed here on purpose. They are the demonstration: when prose and source disagree, the source is right. The comment trail inside the files is the primary documentation, and this course was only ever a guided reading of it. Every rule you learned was justified by a comment or a test that still lives in the tree; if you ever cannot find that justification, you have found either a stale lesson or a deleted safeguard, and both are worth a pull request.

Where to go next​

You have the map. Six hand-offs, depending on what you want to do with it:

  • Build something. Build a module page is the step-by-step recipe — migration, backend module, permission wiring, frontend, tests — for the vertical this course taught you to read.
  • Trace the whole stack. The employees walkthrough runs the same feature end to end, from a page load through the controller and service down to the SQL, and it is the fastest way to check that the map really is in your head.
  • Look things up. The Backend reference is the daily companion — what an endpoint takes, what is already configured — with Security for the authentication and RBAC detail and Backend tests for the harness.
  • Read the decisions. ADR 0001 for PostgreSQL and Flyway, ADR 0003 for JWT plus rotating refresh tokens, and ADR 0013 for shared-database multi-tenancy — the three this course cited most. The rest are indexed at the ADR list.
  • Read the other half. Frontend 101 decodes the browser side of the same system, and its Part 2 reads from the browser the endpoint you decoded in Part 2 here.
  • Ask what calls what. The generated Code Wiki on the documentation site indexes every endpoint and every table straight from the source, which is the one reference that cannot go stale the way this chapter will.

That is the course. The tree is in front of you, and the comments inside it are the rest of the documentation.