For simple objects like bullets, that rarely expand their roles and do other things, I like to keep a small buffer of object slots.
bullet_x: .res (2 * NUM_BULLETS)
bullet_y: .res (2 * NUM_BULLETS)
bullet_dx: .res (1 * NUM_BULLETS)
bullet_dy: .res (1 * NUM_BULLETS)
bullet_state: .res (1 * NUM_BULLETS)
Then, a few states enums are defined:
BULLET_STATE_INACTIVE = 0
BULLET_STATE_ACTIVE = 1
If you want to spawn a bullet, you iterate over your bullets until you find one who isn't busy (or has been despawned):
lda bullet_state, x
cmp #BULLET_STATE_INACTIVE ; this can be skipped since it's a comparison to zero, but...
; We found a bullet, ID number is in X. for this example we mark it as active.
sta bullet_state, x
; Bullet not found
Despawning is a matter of setting bullet_active to 0 for that bullet. Any property of a bullet is indexed by X specifying the bullet.
Then, you can have functions that act on certain bullets with X as a parameter for which bullet, then loop through your bullets for logic and render setup.