GCP vs AWS for a Java developer: what actually changes

Arriving on a GCP engagement with AWS reflexes works fine, until the day a structural detail (not a service name) breaks an assumption you didn't know you'd made. I ended up formalizing these gaps in GCPTrainer, not to cram for a certification, but for what actually matters on an engagement: the transposition traps.
The VPC doesn't have the same scope#
This is the costliest gap to discover in production rather than ahead of time:
| AWS | GCP | |
|---|---|---|
| VPC scope | Regional | Global |
| Subnet scope | Per availability zone | Regional |
| Consequence | One VPC per region, explicitly connected (peering, Transit Gateway) | A single VPC can span all regions, regional subnets are enough to distribute resources |
A developer coming from AWS often reproduces the "one VPC per region" reflex on GCP, adding peering complexity for a problem GCP natively solves by design. Conversely, assuming a GCP VPC behaves like a regional AWS VPC means underestimating its actual scope when designing across regions.
IAM: two different mental models, not just two syntaxes#
AWS reasons in JSON policy documents attached to a principal (user, role, group), with explicit allow and deny rules evaluated together. GCP reasons in a resource hierarchy: organization, folders, projects, resources, with role bindings that automatically inherit from the level above, except for a resource covered by an explicit deny policy (rare, reserved for critical cases).
AWS: principal → policy document → explicit permissions
GCP: organization → folder → project → resource
(a role binding at one level applies to everything beneath it)
The practical consequence: on GCP, a role granted at the project level automatically propagates to every resource it contains, with no extra action. A developer used to AWS expects to attach a policy to each individual resource, and sometimes discovers too late that an access granted higher up the hierarchy was already sufficient, or conversely far too broad.
Compute doesn't map one to one#
| Need | AWS | GCP |
|---|---|---|
| Classic VM | EC2 | Compute Engine |
| Orchestrated containers | ECS or EKS | GKE |
| Event-driven function | Lambda | Cloud Functions |
| On-demand container, scales to zero | Fargate (via ECS) | Cloud Run |
Cloud Run deserves a special mention: it covers what AWS splits between Lambda (short functions) and Fargate (long-running containers), scaling a plain HTTP container from zero to N instances. A developer looking for "the Lambda equivalent" on GCP sometimes settles on Cloud Functions when Cloud Run, more flexible, would have better served a need that goes beyond a simple event-driven function.
What doesn't translate at all#
DynamoDB and Firestore are often presented as equivalent because both are managed NoSQL databases. That's only true on the surface: DynamoDB exposes a key-value model with fine-grained control over partitioning and consistency, built for predictable access at very high scale. Firestore exposes a document model geared toward rich queries and real-time sync, closer to direct application use than a low-level data engine. Migrating a DynamoDB schema to Firestore without rethinking the queries is a classic source of unpleasant performance surprises.
What this changes in practice#
The lesson that goes beyond GCP and AWS themselves: two services whose names translate almost perfectly can carry incompatible structural models. Before transposing an architecture from one cloud to the other, the question to ask is never "what's the equivalent of X," but "what structural constraint does X impose, and does the other platform impose the same one." That question, not the service name, determines whether the transposition actually holds up.


