Items come from a finite set of item definitions from the cache. Imagine all the properties of an item contained into one class...

That is ItemDefinitions.java. All items are simply variations of this class. Take a look...

public final class ItemDefinitions {  
    private static final HashMap<Integer, ItemDefinitions> ITEM_DEFINITIONS = new HashMap();  

    public ItemDefinitions(int id) {
        this(Cache.STORE, id, true);
    }

    public int id;  
    public boolean loaded;  
    public int value;  
    public int modelId;  
    public int modelZoom;  
    public int modelRotationX;  
    public int modelRotationY;  
    public int modelRotationZ;  
    public int modelOffsetX;  
    public int modelOffsetY;  
    public int[] originalModelColors;  
    public int[] modifiedModelColors;  
    public byte[] spriteRecolorIndices;  
    public int[] originalTextureIds;  
    public int[] modifiedTextureIds;  
    public String name;  
    public boolean membersOnly;  
    public int wearPos;  
    public int wearPos2;  
    public int wearPos3;  
    public int maleEquip1;  
    public int maleEquip2;  
    public int maleEquip3;  
    public int femaleEquip1;  
    public int femaleEquip2;  
    public int femaleEquip3;  
    public int maleWearXOffset;  
    public int femaleWearXOffset;  
    public int maleWearYOffset;  
    public int femaleWearYOffset;  
    public int maleWearZOffset;  
    public int femaleWearZOffset;  
    public int maleHead1;  
    public int maleHead2;  
    public int femaleHead1;  
    public int femaleHead2;  
    public int teamId;  
    public String[] groundOptions;  
    public int stackable;  
    public String[] inventoryOptions;  
    public int multiStackSize;  
    public int tooltipColor;  
    public boolean hasTooltipColor;  
    private boolean grandExchange;  
    public int unknownInt6;  
    public int certId;  
    public int certTemplateId;  
    public int resizeX;  
    public int[] stackIds;  
    public int[] stackAmounts;  
    public int resizeY;  
    public int resizeZ;  
    public int ambient;  
    public int contrast;  
    public int lendId;  
    public int lendTemplateId;  
    public int unknownInt18;  
    public int unknownInt19;  
    public int unknownInt20;  
    public int unknownInt21;  
    public int customCursorOp1;  
    public int customCursorId1;  
    public int customCursorOp2;  
    public int customCursorId2;  
    public int[] quests;  
    public int[] bonuses;  
    public int pickSizeShift;  
    public int bindId;  
    public int bindTemplateId;  
    public boolean noted;  
    public boolean lended;  
    private HashMap<Integer, Object> params;  
    private HashMap<Integer, Integer> wieldRequirements;  
    public static final int STAB_ATTACK = 0;  
    public static final int SLASH_ATTACK = 1;  
    public static final int CRUSH_ATTACK = 2;  
    public static final int RANGE_ATTACK = 4;  
    public static final int MAGIC_ATTACK = 3;  
    public static final int STAB_DEF = 5;  
    public static final int SLASH_DEF = 6;  
    public static final int CRUSH_DEF = 7;  
    public static final int RANGE_DEF = 9;  
    public static final int MAGIC_DEF = 8;  
    public static final int SUMMONING_DEF = 10;  
    public static final int STRENGTH_BONUS = 14;  
    public static final int RANGED_STR_BONUS = 15;  
    public static final int MAGIC_DAMAGE = 17;  
    public static final int PRAYER_BONUS = 16;  
    public static final int ABSORVE_MELEE_BONUS = 11;  
    public static final int ABSORVE_RANGE_BONUS = 13;  
    public static final int ABSORVE_MAGE_BONUS = 12;

Pretty long list of properties right? This is how items are defined from the cache. In this way you would be limited to vanilla items, but if you wanted to create custom items without touching the cache you would manipulate these ItemDefinitions for the class and voila, unique item.

Creating vanilla items

First thing to understand, is the simplicity of making a new Item. All it is is an id and amount, new Item(995, 10) would be 10 coins. You simply are making predefined items. With this abstraction in mind take a look at this...

public class Item {   
  private short id;  
	protected int amount;  
  private Map<String, Object> metaData;

  public int getId() { return this.id; }

  public Item(int id) { this(id, 1); }

  public Item(int id, int amount) { this(id, amount, false); }

  public ItemDefinitions getDefinitions() { return ItemDefinitions.getDefs(this.id); }
  
  public Item setAmount(int amount) { /*...*/ }

  public void setId(int id) { this.id = (short)id; }

  public int getAmount() { return this.amount; }

  public String getName() { return this.getDefinitions().getName(); }

	public Item addMetaData(String key, Object value) { /*...*/ }

	public Map<String, Object> getMetaData() { return this.metaData; }

}

Items in vanilla are simply new Item(id, amount);.

Metadata

In the code above you should see a property, metaData. These are simply a map saved to the item, saved to the player file on MongoDB. The map connects a string to any object, allowing some type of saving.

DragonFire shield is an example of this. You save fire breath charges into the metadata. Another is chaotics, you save the charges in it. Or effigies, where you save the stage of the effigy inside it.

private static void chargeDragonfireShield(Entity target) {
	if (target instanceof Player p) {
		int shield = p.getEquipment().getShieldId();
    Item dfs = p.getEquipment().getItem(Equipment.SHIELD);
    if (dfs != null) {
      int charges = dfs.getMetaDataI("dfsCharges");
      if (charges < 50) {
        p.getEquipment().getItem(Equipment.SHIELD).addMetaData("dfsCharges", charges + 1);
        p.setNextAnimation(new Animation(6695));
        p.setNextSpotAnim(new SpotAnim(1164));
        p.sendMessage("Your shield becomes a little stronger as it absorbs the dragonfire.", true);
        p.getCombatDefinitions().refreshBonuses();
      }
    }
	}
}

Something to note, you can manipulate an existing item and change its properties or you can make a new item. There is a difference between these two ways of changing an items properties.

Item whip = new Item(4151, 1);
whip.setAmount(2);

//... Or ...

Item whip2 = new Item(4151, 1);
whip2 = new Item(4151, 2);

The big difference is you lose the metadata.