diff --git a/dapps/valuetransfers/packages/balance/balance.go b/dapps/valuetransfers/packages/balance/balance.go
index 142b95f595219b08f45b2171dc2932d4076caf57..6559145b2b172898e42831a9b31e9bda99520804 100644
--- a/dapps/valuetransfers/packages/balance/balance.go
+++ b/dapps/valuetransfers/packages/balance/balance.go
@@ -8,15 +8,17 @@ import (
 
 // Balance represents a balance in the IOTA ledger. It consists out of a numeric value and a color.
 type Balance struct {
-	value int64
-	color Color
+	// The numeric value of the balance.
+	Value int64 `json:"value"`
+	// The color of the balance.
+	Color Color `json:"color"`
 }
 
 // New creates a new Balance with the given details.
 func New(color Color, balance int64) (result *Balance) {
 	result = &Balance{
-		color: color,
-		value: balance,
+		Color: color,
+		Value: balance,
 	}
 
 	return
@@ -28,7 +30,7 @@ func FromBytes(bytes []byte) (result *Balance, consumedBytes int, err error) {
 
 	marshalUtil := marshalutil.New(bytes)
 
-	result.value, err = marshalUtil.ReadInt64()
+	result.Value, err = marshalUtil.ReadInt64()
 	if err != nil {
 		return
 	}
@@ -40,7 +42,7 @@ func FromBytes(bytes []byte) (result *Balance, consumedBytes int, err error) {
 		return nil, marshalUtil.ReadOffset(), colorErr
 	}
 
-	result.color = coinColor.(Color)
+	result.Color = coinColor.(Color)
 	consumedBytes = marshalUtil.ReadOffset()
 
 	return
@@ -56,29 +58,19 @@ func Parse(marshalUtil *marshalutil.MarshalUtil) (*Balance, error) {
 	return address.(*Balance), nil
 }
 
-// Value returns the numeric value of the balance.
-func (balance *Balance) Value() int64 {
-	return balance.value
-}
-
-// Color returns the Color of the balance.
-func (balance *Balance) Color() Color {
-	return balance.color
-}
-
 // Bytes marshals the Balance into a sequence of bytes.
 func (balance *Balance) Bytes() []byte {
 	marshalUtil := marshalutil.New(Length)
 
-	marshalUtil.WriteInt64(balance.value)
-	marshalUtil.WriteBytes(balance.color.Bytes())
+	marshalUtil.WriteInt64(balance.Value)
+	marshalUtil.WriteBytes(balance.Color.Bytes())
 
 	return marshalUtil.Bytes()
 }
 
 // String creates a human readable string of the Balance.
 func (balance *Balance) String() string {
-	return strconv.FormatInt(balance.value, 10) + " " + balance.color.String()
+	return strconv.FormatInt(balance.Value, 10) + " " + balance.Color.String()
 }
 
 // Length encodes the length of a marshaled Balance (the length of the color + 8 bytes for the balance).
diff --git a/dapps/valuetransfers/packages/balance/balance_test.go b/dapps/valuetransfers/packages/balance/balance_test.go
index 5dd98567cf634ec3ad3bd8ede4a42ed6d0ab4932..35401c66a5eb5bed4ace5281f4ff1de1e289feb7 100644
--- a/dapps/valuetransfers/packages/balance/balance_test.go
+++ b/dapps/valuetransfers/packages/balance/balance_test.go
@@ -8,8 +8,8 @@ import (
 
 func TestMarshalUnmarshal(t *testing.T) {
 	balance := New(ColorIOTA, 1337)
-	assert.Equal(t, int64(1337), balance.Value())
-	assert.Equal(t, ColorIOTA, balance.Color())
+	assert.Equal(t, int64(1337), balance.Value)
+	assert.Equal(t, ColorIOTA, balance.Color)
 
 	marshaledBalance := balance.Bytes()
 	assert.Equal(t, Length, len(marshaledBalance))
@@ -19,6 +19,6 @@ func TestMarshalUnmarshal(t *testing.T) {
 		panic(err)
 	}
 	assert.Equal(t, Length, consumedBytes)
-	assert.Equal(t, balance.value, restoredBalance.Value())
-	assert.Equal(t, balance.color, restoredBalance.Color())
+	assert.Equal(t, balance.Value, restoredBalance.Value)
+	assert.Equal(t, balance.Color, restoredBalance.Color)
 }
diff --git a/dapps/valuetransfers/packages/tangle/ledgerstate.go b/dapps/valuetransfers/packages/tangle/ledgerstate.go
index 5c0d8c31a7b7bd0c93dcb01aaa29c35719806c22..9b676acfedfebbf7a417bfc8d63c0bf24bdc8991 100644
--- a/dapps/valuetransfers/packages/tangle/ledgerstate.go
+++ b/dapps/valuetransfers/packages/tangle/ledgerstate.go
@@ -25,7 +25,7 @@ func (ledgerState *LedgerState) Balances(address address.Address) (coloredBalanc
 	ledgerState.tangle.OutputsOnAddress(address).Consume(func(output *Output) {
 		if output.ConsumerCount() == 0 {
 			for _, coloredBalance := range output.Balances() {
-				coloredBalances[coloredBalance.Color()] += coloredBalance.Value()
+				coloredBalances[coloredBalance.Color] += coloredBalance.Value
 			}
 		}
 	})
diff --git a/dapps/valuetransfers/packages/tangle/snapshot.go b/dapps/valuetransfers/packages/tangle/snapshot.go
new file mode 100644
index 0000000000000000000000000000000000000000..b13df4d81409167068c882c8c893767c030cfcaa
--- /dev/null
+++ b/dapps/valuetransfers/packages/tangle/snapshot.go
@@ -0,0 +1,125 @@
+package tangle
+
+import (
+	"encoding/binary"
+	"fmt"
+	"io"
+
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction"
+)
+
+// Snapshot defines a snapshot of the ledger state.
+type Snapshot map[transaction.ID]map[address.Address][]*balance.Balance
+
+// WriteTo writes the snapshot data to the given writer in the following format:
+// 	transaction_count(int64)
+//	-> transaction_count * transaction_id(32byte)
+//		->address_count(int64)
+//			->address_count * address(33byte)
+//				->balance_count(int64)
+//					->balance_count * value(int64)+color(32byte)
+func (s Snapshot) WriteTo(writer io.Writer) (int64, error) {
+	var bytesWritten int64
+	transactionCount := len(s)
+	if err := binary.Write(writer, binary.LittleEndian, int64(transactionCount)); err != nil {
+		return 0, fmt.Errorf("unable to write transactions count: %w", err)
+	}
+	bytesWritten += 8
+	for txID, addresses := range s {
+		if err := binary.Write(writer, binary.LittleEndian, txID); err != nil {
+			return bytesWritten, fmt.Errorf("unable to write transaction ID: %w", err)
+		}
+		bytesWritten += transaction.IDLength
+		if err := binary.Write(writer, binary.LittleEndian, int64(len(addresses))); err != nil {
+			return bytesWritten, fmt.Errorf("unable to write address count: %w", err)
+		}
+		bytesWritten += 8
+		for addr, balances := range addresses {
+			if err := binary.Write(writer, binary.LittleEndian, addr); err != nil {
+				return bytesWritten, fmt.Errorf("unable to write address: %w", err)
+			}
+			bytesWritten += address.Length
+			if err := binary.Write(writer, binary.LittleEndian, int64(len(balances))); err != nil {
+				return bytesWritten, fmt.Errorf("unable to write balance count: %w", err)
+			}
+			bytesWritten += 8
+			for _, bal := range balances {
+				if err := binary.Write(writer, binary.LittleEndian, bal.Value); err != nil {
+					return bytesWritten, fmt.Errorf("unable to write balance value: %w", err)
+				}
+				bytesWritten += 8
+				if err := binary.Write(writer, binary.LittleEndian, bal.Color); err != nil {
+					return bytesWritten, fmt.Errorf("unable to write balance color: %w", err)
+				}
+				bytesWritten += balance.ColorLength
+			}
+		}
+	}
+
+	return bytesWritten, nil
+}
+
+// ReadFrom reads the snapshot bytes from the given reader.
+// This function overrides existing content of the snapshot.
+func (s Snapshot) ReadFrom(reader io.Reader) (int64, error) {
+	var bytesRead int64
+	var transactionCount int64
+	if err := binary.Read(reader, binary.LittleEndian, &transactionCount); err != nil {
+		return 0, fmt.Errorf("unable to read transaction count: %w", err)
+	}
+	bytesRead += 8
+
+	var i int64
+	for ; i < transactionCount; i++ {
+		txIDBytes := make([]byte, transaction.IDLength)
+		if err := binary.Read(reader, binary.LittleEndian, txIDBytes); err != nil {
+			return bytesRead, fmt.Errorf("unable to read transaction ID: %w", err)
+		}
+		bytesRead += transaction.IDLength
+		var addrCount int64
+		if err := binary.Read(reader, binary.LittleEndian, &addrCount); err != nil {
+			return bytesRead, fmt.Errorf("unable to read address count: %w", err)
+		}
+		bytesRead += 8
+		txAddrMap := make(map[address.Address][]*balance.Balance, addrCount)
+		var j int64
+		for ; j < addrCount; j++ {
+			addrBytes := make([]byte, address.Length)
+			if err := binary.Read(reader, binary.LittleEndian, addrBytes); err != nil {
+				return bytesRead, fmt.Errorf("unable to read address: %w", err)
+			}
+			bytesRead += address.Length
+			var balanceCount int64
+			if err := binary.Read(reader, binary.LittleEndian, &balanceCount); err != nil {
+				return bytesRead, fmt.Errorf("unable to read balance count: %w", err)
+			}
+			bytesRead += 8
+
+			balances := make([]*balance.Balance, balanceCount)
+			var k int64
+			for ; k < balanceCount; k++ {
+				var value int64
+				if err := binary.Read(reader, binary.LittleEndian, &value); err != nil {
+					return bytesRead, fmt.Errorf("unable to read balance value: %w", err)
+				}
+				bytesRead += 8
+				color := balance.Color{}
+				if err := binary.Read(reader, binary.LittleEndian, &color); err != nil {
+					return bytesRead, fmt.Errorf("unable to read balance color: %w", err)
+				}
+				bytesRead += balance.ColorLength
+				balances[k] = &balance.Balance{Value: value, Color: color}
+			}
+			addr := address.Address{}
+			copy(addr[:], addrBytes)
+			txAddrMap[addr] = balances
+		}
+		txID := transaction.ID{}
+		copy(txID[:], txIDBytes)
+		s[txID] = txAddrMap
+	}
+
+	return bytesRead, nil
+}
diff --git a/dapps/valuetransfers/packages/tangle/tangle.go b/dapps/valuetransfers/packages/tangle/tangle.go
index 3ff44e6d32a81779fdc21cf04eba0a987b42c392..21931cc9098b0821d52acd4c1a680e5e4285bcae 100644
--- a/dapps/valuetransfers/packages/tangle/tangle.go
+++ b/dapps/valuetransfers/packages/tangle/tangle.go
@@ -165,9 +165,6 @@ func (tangle *Tangle) BranchManager() *branchmanager.BranchManager {
 	return tangle.branchManager
 }
 
-// Snapshot defines a snapshot of the ledger state.
-type Snapshot map[transaction.ID]map[address.Address][]*balance.Balance
-
 // LoadSnapshot creates a set of outputs in the value tangle, that are forming the genesis for future transactions.
 func (tangle *Tangle) LoadSnapshot(snapshot Snapshot) {
 	for transactionID, addressBalances := range snapshot {
@@ -1520,9 +1517,9 @@ func (tangle *Tangle) retrieveConsumedInputDetails(tx *transaction.Transaction)
 		// calculate the input balances
 		for _, inputBalance := range input.Balances() {
 			var newBalance int64
-			if currentBalance, balanceExists := consumedBalances[inputBalance.Color()]; balanceExists {
+			if currentBalance, balanceExists := consumedBalances[inputBalance.Color]; balanceExists {
 				// check overflows in the numbers
-				if inputBalance.Value() > math.MaxInt64-currentBalance {
+				if inputBalance.Value > math.MaxInt64-currentBalance {
 					// TODO: make it an explicit error var
 					err = fmt.Errorf("buffer overflow in balances of inputs")
 
@@ -1531,11 +1528,11 @@ func (tangle *Tangle) retrieveConsumedInputDetails(tx *transaction.Transaction)
 					return
 				}
 
-				newBalance = currentBalance + inputBalance.Value()
+				newBalance = currentBalance + inputBalance.Value
 			} else {
-				newBalance = inputBalance.Value()
+				newBalance = inputBalance.Value
 			}
-			consumedBalances[inputBalance.Color()] = newBalance
+			consumedBalances[inputBalance.Color] = newBalance
 		}
 	}
 	inputsSolid = true
@@ -1556,51 +1553,51 @@ func (tangle *Tangle) checkTransactionOutputs(inputBalances map[balance.Color]in
 	aborted := !outputs.ForEach(func(address address.Address, balances []*balance.Balance) bool {
 		for _, outputBalance := range balances {
 			// abort if the output creates a negative or empty output
-			if outputBalance.Value() <= 0 {
+			if outputBalance.Value <= 0 {
 				return false
 			}
 
 			// sidestep logic if we have a newly colored output (we check the supply later)
-			if outputBalance.Color() == balance.ColorNew {
+			if outputBalance.Color == balance.ColorNew {
 				// catch overflows
-				if newlyColoredCoins > math.MaxInt64-outputBalance.Value() {
+				if newlyColoredCoins > math.MaxInt64-outputBalance.Value {
 					return false
 				}
 
-				newlyColoredCoins += outputBalance.Value()
+				newlyColoredCoins += outputBalance.Value
 
 				continue
 			}
 
 			// sidestep logic if we have ColorIOTA
-			if outputBalance.Color() == balance.ColorIOTA {
+			if outputBalance.Color == balance.ColorIOTA {
 				// catch overflows
-				if uncoloredCoins > math.MaxInt64-outputBalance.Value() {
+				if uncoloredCoins > math.MaxInt64-outputBalance.Value {
 					return false
 				}
 
-				uncoloredCoins += outputBalance.Value()
+				uncoloredCoins += outputBalance.Value
 
 				continue
 			}
 
 			// check if the used color does not exist in our supply
-			availableBalance, spentColorExists := inputBalances[outputBalance.Color()]
+			availableBalance, spentColorExists := inputBalances[outputBalance.Color]
 			if !spentColorExists {
 				return false
 			}
 
 			// abort if we spend more coins of the given color than we have
-			if availableBalance < outputBalance.Value() {
+			if availableBalance < outputBalance.Value {
 				return false
 			}
 
 			// subtract the spent coins from the supply of this color
-			inputBalances[outputBalance.Color()] -= outputBalance.Value()
+			inputBalances[outputBalance.Color] -= outputBalance.Value
 
 			// cleanup empty map entries (we have exhausted our funds)
-			if inputBalances[outputBalance.Color()] == 0 {
-				delete(inputBalances, outputBalance.Color())
+			if inputBalances[outputBalance.Color] == 0 {
+				delete(inputBalances, outputBalance.Color)
 			}
 		}
 
diff --git a/dapps/valuetransfers/packages/tangle/tangle_snapshot_test.go b/dapps/valuetransfers/packages/tangle/tangle_snapshot_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..4011f34d9ecf46c1e0e4bcdb055619726cc5efea
--- /dev/null
+++ b/dapps/valuetransfers/packages/tangle/tangle_snapshot_test.go
@@ -0,0 +1,88 @@
+package tangle
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet"
+	"github.com/iotaledger/hive.go/kvstore/mapdb"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLoadSnapshot(t *testing.T) {
+	tangle := New(mapdb.NewMapDB())
+
+	snapshot := map[transaction.ID]map[address.Address][]*balance.Balance{
+		transaction.GenesisID: {
+			address.Random(): []*balance.Balance{
+				balance.New(balance.ColorIOTA, 337),
+			},
+
+			address.Random(): []*balance.Balance{
+				balance.New(balance.ColorIOTA, 1000),
+				balance.New(balance.ColorIOTA, 1000),
+			},
+		},
+	}
+	tangle.LoadSnapshot(snapshot)
+
+	// check whether outputs can be retrieved from tangle
+	for addr, balances := range snapshot[transaction.GenesisID] {
+		cachedOutput := tangle.TransactionOutput(transaction.NewOutputID(addr, transaction.GenesisID))
+		cachedOutput.Consume(func(output *Output) {
+			assert.Equal(t, addr, output.Address())
+			assert.ElementsMatch(t, balances, output.Balances())
+			assert.True(t, output.Solid())
+			assert.Equal(t, branchmanager.MasterBranchID, output.BranchID())
+		})
+	}
+}
+
+func TestSnapshotMarshalUnmarshal(t *testing.T) {
+	const genesisBalance = 3333
+	seed := wallet.NewSeed()
+	genesisAddr := seed.Address(GENESIS)
+
+	snapshot := Snapshot{
+		transaction.GenesisID: {
+			genesisAddr: {
+				balance.New(balance.ColorIOTA, genesisBalance),
+			},
+		},
+	}
+
+	// includes txs count
+	const int64ByteSize = 8
+	expectedLength := int64ByteSize
+	for _, addresses := range snapshot {
+		// tx id
+		expectedLength += transaction.IDLength
+		// addr count
+		expectedLength += int64ByteSize
+		for _, balances := range addresses {
+			// addr
+			expectedLength += address.Length
+			// balance count
+			expectedLength += int64ByteSize
+			// balances
+			expectedLength += len(balances) * (int64ByteSize + balance.ColorLength)
+		}
+	}
+
+	var buf bytes.Buffer
+	written, err := snapshot.WriteTo(&buf)
+	assert.NoError(t, err, "writing the snapshot to the buffer should succeed")
+	assert.EqualValues(t, expectedLength, written, "written byte count should match the expected count")
+
+	snapshotFromBytes := Snapshot{}
+	read, err := snapshotFromBytes.ReadFrom(&buf)
+	assert.NoError(t, err, "expected no error from reading valid snapshot bytes")
+	assert.EqualValues(t, expectedLength, read, "read byte count should match the expected count")
+
+	// check that the source and unmarshaled snapshot are equivalent
+	assert.Equal(t, snapshot, snapshotFromBytes)
+}
diff --git a/dapps/valuetransfers/packages/tangle/tangle_test.go b/dapps/valuetransfers/packages/tangle/tangle_test.go
index b7dc5a90361ec2f8669acba1483ecfc7d2a6a271..85309df19052b3274f3ec86911a9ccdc56109009 100644
--- a/dapps/valuetransfers/packages/tangle/tangle_test.go
+++ b/dapps/valuetransfers/packages/tangle/tangle_test.go
@@ -874,35 +874,6 @@ func TestGetCachedOutputsFromTransactionInputs(t *testing.T) {
 	}
 }
 
-func TestLoadSnapshot(t *testing.T) {
-	tangle := New(mapdb.NewMapDB())
-
-	snapshot := map[transaction.ID]map[address.Address][]*balance.Balance{
-		transaction.GenesisID: {
-			address.Random(): []*balance.Balance{
-				balance.New(balance.ColorIOTA, 337),
-			},
-
-			address.Random(): []*balance.Balance{
-				balance.New(balance.ColorIOTA, 1000),
-				balance.New(balance.ColorIOTA, 1000),
-			},
-		},
-	}
-	tangle.LoadSnapshot(snapshot)
-
-	// check whether outputs can be retrieved from tangle
-	for addr, balances := range snapshot[transaction.GenesisID] {
-		cachedOutput := tangle.TransactionOutput(transaction.NewOutputID(addr, transaction.GenesisID))
-		cachedOutput.Consume(func(output *Output) {
-			assert.Equal(t, addr, output.Address())
-			assert.ElementsMatch(t, balances, output.Balances())
-			assert.True(t, output.Solid())
-			assert.Equal(t, branchmanager.MasterBranchID, output.BranchID())
-		})
-	}
-}
-
 func TestRetrieveConsumedInputDetails(t *testing.T) {
 	// test simple happy case
 	{
@@ -1549,7 +1520,7 @@ func sumOutputsByColor(outputs map[address.Address][]*balance.Balance) map[balan
 
 	for _, balances := range outputs {
 		for _, bal := range balances {
-			totals[bal.Color()] += bal.Value()
+			totals[bal.Color] += bal.Value
 		}
 	}
 
diff --git a/plugins/dashboard/payload_handler.go b/plugins/dashboard/payload_handler.go
index db8ab642269e0f7db2f9161de2a62dbf4d2bb23b..6ffa3593a9927bdeb97c61f3de2d74d05c3d76b4 100644
--- a/plugins/dashboard/payload_handler.go
+++ b/plugins/dashboard/payload_handler.go
@@ -135,8 +135,8 @@ func processValuePayload(p payload.Payload) (vp ValuePayload) {
 		var b []Balance
 		for _, balance := range balances {
 			b = append(b, Balance{
-				Value: balance.Value(),
-				Color: balance.Color().String(),
+				Value: balance.Value,
+				Color: balance.Color.String(),
 			})
 		}
 		t := OutputContent{
diff --git a/plugins/webapi/value/unspentoutputs/handler.go b/plugins/webapi/value/unspentoutputs/handler.go
index e22c9b0a076be495e2816be8b2c268201e823dd8..acb5f5b83ff7f39b2cc176b8d76d792834bfde31 100644
--- a/plugins/webapi/value/unspentoutputs/handler.go
+++ b/plugins/webapi/value/unspentoutputs/handler.go
@@ -38,8 +38,8 @@ func Handler(c echo.Context) error {
 				var b []utils.Balance
 				for _, balance := range output.Balances() {
 					b = append(b, utils.Balance{
-						Value: balance.Value(),
-						Color: balance.Color().String(),
+						Value: balance.Value,
+						Color: balance.Color.String(),
 					})
 				}
 
diff --git a/plugins/webapi/value/utils/transaction_handler.go b/plugins/webapi/value/utils/transaction_handler.go
index 308904d55c3e577c0d6f5cdd08b0d2fecc0590f2..15a587ead8c24e2233a4e6b78249f56146c94fb8 100644
--- a/plugins/webapi/value/utils/transaction_handler.go
+++ b/plugins/webapi/value/utils/transaction_handler.go
@@ -21,8 +21,8 @@ func ParseTransaction(t *transaction.Transaction) (txn Transaction) {
 		var b []Balance
 		for _, balance := range balances {
 			b = append(b, Balance{
-				Value: balance.Value(),
-				Color: balance.Color().String(),
+				Value: balance.Value,
+				Color: balance.Color.String(),
 			})
 		}
 		t := Output{