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.