For most applications, if you generate a random identifier for a piece of data, you need to store that identifier somewhere so you can find the data again later. That's what I expected when I started learning Solana.
Instead, I discovered that Solana programs can derive the same account address whenever they need it. They don't have to store the address at all. That was the part I had been missing about Program Derived Addresses (PDAs), and it completely changed how I think about storing application state on Solana.
The mental model
When I first heard the term Program Derived Address, I assumed it simply meant "an address created by a program." That wasn't entirely wrong, but it missed the point.
A PDA isn't just an address a program creates. It's an address a program can deterministically derive from the same inputs every time.
Those inputs are:
- one or more seeds
- the program's ID
- a bump value
As long as those inputs don't change, the derived address never changes either. The important consequence isn't how the address is generated, it's that the program never needs to remember where your data lives. If it knows the same seeds and its own program ID, it can derive the address again whenever it needs to read or update the account.
That felt very different from what I'm used to, where generating a random identifier usually means storing it somewhere before you can find the data again.
Anatomy of a derivation
Here's what creating the counter account looked like in my program:
#[account(
init,
payer = user,
space = 8 + Counter::INIT_SPACE,
seeds = [b"counter", user.key().as_ref()],
bump
)]
pub counter: Account<'info, Counter>,
Each part has a purpose.
init tells Anchor to create the account if it doesn't already exist.
payer = user specifies which wallet funds the account creation.
space reserves enough bytes to store the account data.
The interesting part is the seeds:
seeds = [b"counter", user.key().as_ref()]
The first seed is a static identifier that tells the program what kind of account it's deriving. The second seed is dynamic; it uses the user's public key. Anchor combines these seeds with the program ID to derive a deterministic address.
That program ID is more important than I initially realized. Even if another program used the exact same seeds, it would derive a completely different PDA because its program ID is different. That means another program can't simply pretend it owns your PDA.
What about the bump?
The bump was probably the least intuitive part for me. I don't fully understand the mathematics behind it yet, but I do understand its role. Not every derived address is valid as a PDA. The runtime searches for a valid address by trying different bump values until it finds one that falls off the Ed25519 curve.
When you write: bump
Anchor automatically derives and stores the canonical bump for you, so you don't have to calculate it yourself. That's enough for me to use PDAs correctly today, even if I couldn't explain the underlying algorithm from memory.
Why the seeds matter
Changing the seeds changes the kind of application you're building. For my counter, I used:
[b"counter", user.key().as_ref()]
Because the user's public key is part of the derivation, every wallet gets its own counter. If I removed the user key and only used:
[b"counter"]
every wallet would derive exactly the same PDA.
Sometimes that's exactly what you want. For example, a global configuration account shared by the entire application. For a personal counter, though, it would be a disaster because every user would be trying to update the same account.
That small change in the seeds completely changes the behavior of the application.
The full lifecycle and payoff
What made this week interesting wasn't just deriving a PDA, it was seeing the entire lifecycle.
- First, derive the address.
- Then initialize the account at that address.
- Update its state through later instructions.
- Finally, close the account when it's no longer needed.
Closing the account - Earlier in the challenge, I assumed the lamports used for rent were a permanent cost. Closing my first PDA taught me they aren't.
When the account is closed, its data is removed, the reserved lamports are refunded to the designated recipient, and the account is cleaned up by the network. That made rent feel much more like a refundable storage deposit than a permanent fee.
Before this week, I assumed the client would need to remember every account address it created. Now I know that isn't necessary. As long as the frontend knows the same seeds and the program ID, it can derive the same PDA whenever it needs to fetch the account. Nothing extra needs to be stored, which makes the architecture feel much cleaner. It also answered another question I had.
Since the program ID is part of the derivation, another program can't generate the same PDA just by knowing the seeds. The same inputs in a different program produce a completely different address.
What I'd tell past me
- The program never stores the account address; it derives it whenever it's needed.
- A PDA has no private key. It's not a wallet.
- The same seeds in a different program produce a different PDA because the program ID is part of the derivation.
- You don't need to fully understand the bump algorithm on day one. Understanding its purpose is enough to start building.
What's next
PDAs solved one of the biggest questions I had about state management on Solana. On the surface, they seem like a small feature. In practice, they fundamentally change how applications manage state. Instead of remembering where data lives, programs can deterministically derive its location whenever they need it. That's a very different way of thinking about application architecture, and it's one of the biggest shifts I've had since I started learning Solana.
If you're learning PDAs yourself, I'd recommend reading the official Solana PDA documentation alongside the Anchor PDA documentation. Both helped reinforce the concepts I explored this week, and the full source code for my counter program is available on GitHub.

Top comments (0)