After spending years building web applications with Rails, I decided to go deeper into Solana development. Not just using wallets and tokens — actually writing on-chain programs. The Anchor framework is the standard tool for this, and like any good learning path, it starts with Hello World.
This post walks through my first Anchor program: what the code actually does, what surprised me coming from a web development background, and the mental model shifts required.
Why Anchor?
Writing raw Solana programs in Rust is possible but painful. You deal with manual serialization, account validation boilerplate, and error-prone byte manipulation. Anchor abstracts all of that while still compiling down to a native Solana program.
Think of it like the difference between writing raw SQL vs using ActiveRecord. Same database, dramatically different developer experience.
The Program
The entire program fits in one file:
use anchor_lang::prelude::*;
declare_id!("AfpbkM2nP73YFUaFSH8dxV2VqrdjfWC48N4R4vkzhzSm");
#[program]
pub mod hello {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
msg!("Greetings from: {:?}", ctx.program_id);
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize {}
That’s it. But there’s more going on than it looks.
Breaking It Down
declare_id!
Every Solana program lives at a specific address on-chain. declare_id! hardcodes that address into the binary. It’s generated the first time you run anchor build — similar to how a deployed Rails app has a fixed URL, except here it’s a cryptographic public key.
The #[program] Module
The #[program] macro marks the module as containing instruction handlers. In Solana terms, an “instruction” is a function that gets called when someone sends a transaction to your program. It’s roughly equivalent to a controller action in Rails.
initialize is the instruction handler here. It takes a Context<Initialize> (which provides access to accounts and the program ID) and returns Result<()>. The msg! macro logs to Solana’s transaction logs — the on-chain equivalent of Rails.logger.info.
The Accounts Struct
#[derive(Accounts)]
pub struct Initialize {}
This is where Solana’s model diverges from anything in web development. Every piece of state on Solana lives in an “account.” Programs don’t have databases — they read and write to accounts that are passed in with each transaction.
The Initialize struct defines which accounts this instruction expects. It’s empty here because Hello World doesn’t need any state. But in real programs, this is where you define account constraints, permissions, and relationships.
Testing
Anchor generates test scaffolding. The Rust test creates a client, connects to a local validator, and sends a transaction:
let tx = program
.request()
.accounts(hello::accounts::Initialize {})
.args(hello::instruction::Initialize {})
.send()
.expect("Transaction failed");
Running anchor test spins up a local Solana validator, deploys the program, and runs the tests. The whole cycle takes a few seconds.
What Surprised Me
Everything is an account. Coming from Rails where you think in terms of database tables and rows, Solana’s account model is a different paradigm. Programs are accounts. State is stored in accounts. Even the program’s executable code lives in an account. It takes a while to internalize.
No implicit state. In Rails, your model can query the database whenever it wants. In Solana, every piece of data the program needs must be explicitly passed in as an account in the transaction. Nothing is implicit.
Compile-time guarantees. Anchor validates account constraints at compile time. If you forget to mark an account as mutable, or forget to include the system program, the compiler catches it. After years of runtime errors in Ruby, this feels like a superpower.
What’s Next
Hello World is just the skeleton. The next step is building something with actual state — an account that stores data and has access control. That’s the price oracle program.
The code is on GitHub: anchor-hello-world