Cassandra rewards you for designing tables around queries — and punishes you for treating it like a relational database. The partition key is the unit of distribution AND the unit of locality; everything else flows from that one decision.
Query first, not entity first
In Postgres you start with entities (User, Order) and add indexes for queries. In Cassandra you start with the queries: 'show me last 30 days of orders for a user' becomes a table; 'show me all orders in a region' becomes a different table. Denormalization is mandatory, not a smell.
Partition key = locality
All rows with the same partition key live on the same node (and its replicas). Reads against a single partition are fast (one node hop). Reads spanning partitions become coordinator-mediated scatter-gathers — slow and fragile. Aim to answer 95% of queries with single-partition reads.
Clustering columns = order
Within a partition, clustering columns define on-disk order. Choose them to match how you'll scan: time DESC for timeline reads, status ASC for state machines. The clustering key also enforces uniqueness within the partition.
Anti-patterns
Wide partitions (>100MB or >100K rows): repair lag, JVM pressure, slow streaming. Tombstone storms (deleting many rows in a partition you keep reading): scan times blow up. Secondary indexes on high-cardinality columns: don't — duplicate the table instead.
Practical sizing rule
Target partition size: 10-50MB, <100K rows. If you can't, time-bucket the partition key (user_id, year_month) and query multiple buckets in parallel from the app. Most production pain comes from skipping this exercise.