Context
This is a team rebuild. The overall direction, a dedicated identity provider with policy-based authorization, and the standards behind it were set by our architect. My part was the layer underneath: turning that standard into a concrete, buildable design for the transport domain, the analysis of the legacy system we were moving off, and the build that followed with the team.
The transport platform is being rebuilt as an ecosystem of microservices replacing a legacy monolith. In the old world, authentication and authorization were handled by a single shared gateway with a static, role-based access model: a fixed set of roles, wired in at the edge, applied uniformly across everything behind it.
That holds up until the system grows. Every new service inherited the same coarse role set, and every access change meant editing the one shared component that everything else sat behind. The rebuild was the chance to redo the access layer properly instead of carrying the old gateway forward.
The problem
A static-RBAC gateway couples identity (who is calling) to authorization (what they may do) and concentrates both in one shared place. As the platform grows, that model breaks down on two fronts:
- Roles can't express real access. To let someone act only within their own slice of the system, an admin had to mint a dedicated directory group for that scope. Every region, role, and application turned into another group to create and keep in sync, and that group membership was the authorization. There was no clean way to say "this much, in this app, for this scope" without creating yet another group.
- One shared chokepoint. Every team's access changes converge on the same component, so the model couples otherwise-unrelated services together, and that shared surface only grows busier as the platform does.
And the fix couldn't be a flag-day cutover: an entire live ecosystem had to move to the new model incrementally, service by service.
Options considered
Keep the static gateway, add roles
Cheapest in the short term: extend the existing role catalogue and middleware. Rejected because it doubles down on the coupling that was already failing and pushes the real cost into every future service.
Centralized authorization service (relationship-based)
A dedicated authz service modelling permissions as a relationship graph was a serious contender, expressive and good for fine-grained sharing. The trade-off is operational: another stateful service in the hot path, and a data model the consuming teams have to learn and keep in sync.
Policy-based authorization with a dedicated identity provider
Split the concern cleanly: a real identity provider (OIDC/SAML) owns authentication and token issuance, and policy-as-code owns authorization, evaluated per application, close to each service, instead of at one shared edge.
The decision, and why
We went with the policy-based model: a dedicated identity provider for authentication and token issuance, policy-as-code for authorization, fronted by an API gateway provisioned as code. The deciding factors:
- Decoupling. Identity and authorization stopped being the same thing. The identity provider federates the existing corporate logins for authentication, while authorization moved out of group membership and into policy-as-code, evaluated per application.
- Per-application policy. Each service expresses its own access rules as policy-as-code rather than negotiating for slots in a shared role catalogue.
- Auditability. Policy-as-code is reviewable, diffable, and testable. Access decisions stop being implicit middleware behaviour.
The enterprise standard was not mine to claim. The move to a dedicated identity provider with policy-based authorization, and the specific product choices behind it, were set by our architect and the wider platform direction. What was mine is the layer underneath: turning that abstract standard into a concrete, buildable design for the transport domain. I authored the domain solution design (the options and trade-offs above, the token and authorization flows, the network isolation, and the incremental path off the legacy gateway), then built it with the team. That is the honest split: the standard is the architect's, the domain design is mine, the build is the team's and mine.
Migration without a flag-day
In a cutover like this, the risk lives in the transition rather than the new design. Services move incrementally behind the new identity provider and policy layer, validating each app's access in place before retiring its dependence on the old static gateway, so the old gateway is dismantled in steps rather than switched off in one risky cut.
Where it stands
This is live for the first domains rather than still on paper, with more following the same incremental path off the old shared gateway.
Because the same pattern has to land in more than one team, I take part in a cross-team effort to share the identity and access-control operational knowledge, so each team adopting the model inherits the edge cases others have already hit instead of rediscovering them alone.
Reflection
The part that stuck with me: on an access platform, the policy engine is rarely the hard bit. The migration and the separation of concerns are. Choosing the tools is close to a one-line decision; understanding the old system well enough to move off it safely, designing the domain migration around it, and doing that incrementally as a team is the real work. Doing that design next to the people setting the enterprise standard, and learning how those calls are made, is exactly the direction I want to grow.