diff --git a/dapps/valuetransfers/dapp.go b/dapps/valuetransfers/dapp.go
index e15bd11a7700132e6b8fe699a90496b8b8d390e7..6aaf256bede9688549cd77aeb593c31f9b4b0027 100644
--- a/dapps/valuetransfers/dapp.go
+++ b/dapps/valuetransfers/dapp.go
@@ -1,8 +1,11 @@
 package valuetransfers
 
 import (
+	"sync"
 	"time"
 
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tipmanager"
 	"github.com/iotaledger/goshimmer/plugins/database"
 	"github.com/iotaledger/hive.go/daemon"
 	"github.com/iotaledger/hive.go/events"
@@ -42,6 +45,12 @@ var (
 
 	// log holds a reference to the logger used by this app.
 	log *logger.Logger
+
+	tipManager     *tipmanager.TipManager
+	tipManagerOnce sync.Once
+
+	valueObjectFactory     *tangle.ValueObjectFactory
+	valueObjectFactoryOnce sync.Once
 )
 
 func configure(_ *node.Plugin) {
@@ -54,6 +63,19 @@ func configure(_ *node.Plugin) {
 		log.Error(err)
 	}))
 
+	// initialize tip manager and value object factory
+	tipManager = TipManager()
+	valueObjectFactory = ValueObjectFactory()
+
+	Tangle.Events.PayloadLiked.Attach(events.NewClosure(func(cachedPayload *payload.CachedPayload, cachedMetadata *tangle.CachedPayloadMetadata) {
+		cachedMetadata.Release()
+		cachedPayload.Consume(tipManager.AddTip)
+	}))
+	Tangle.Events.PayloadDisliked.Attach(events.NewClosure(func(cachedPayload *payload.CachedPayload, cachedMetadata *tangle.CachedPayloadMetadata) {
+		cachedMetadata.Release()
+		cachedPayload.Consume(tipManager.RemoveTip)
+	}))
+
 	// configure FCOB consensus rules
 	FCOB = consensus.NewFCOB(Tangle, AverageNetworkDelay)
 	FCOB.Events.Vote.Attach(events.NewClosure(func(id string, initOpn vote.Opinion) {
@@ -110,3 +132,19 @@ func onReceiveMessageFromMessageLayer(cachedMessage *message.CachedMessage, cach
 
 	Tangle.AttachPayload(valuePayload)
 }
+
+// TipManager returns the TipManager singleton.
+func TipManager() *tipmanager.TipManager {
+	tipManagerOnce.Do(func() {
+		tipManager = tipmanager.New()
+	})
+	return tipManager
+}
+
+// ValueObjectFactory returns the ValueObjectFactory singleton.
+func ValueObjectFactory() *tangle.ValueObjectFactory {
+	valueObjectFactoryOnce.Do(func() {
+		valueObjectFactory = tangle.NewValueObjectFactory(TipManager())
+	})
+	return valueObjectFactory
+}
diff --git a/dapps/valuetransfers/packages/tangle/factory.go b/dapps/valuetransfers/packages/tangle/factory.go
new file mode 100644
index 0000000000000000000000000000000000000000..6720e5d2f9c4361a3a4947706db17e848f16ffd1
--- /dev/null
+++ b/dapps/valuetransfers/packages/tangle/factory.go
@@ -0,0 +1,45 @@
+package tangle
+
+import (
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tipmanager"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction"
+	"github.com/iotaledger/hive.go/events"
+)
+
+// ValueObjectFactory acts as a factory to create new value objects.
+type ValueObjectFactory struct {
+	tipManager *tipmanager.TipManager
+	Events     *ValueObjectFactoryEvents
+}
+
+// NewValueObjectFactory creates a new ValueObjectFactory.
+func NewValueObjectFactory(tipManager *tipmanager.TipManager) *ValueObjectFactory {
+	return &ValueObjectFactory{
+		tipManager: tipManager,
+		Events: &ValueObjectFactoryEvents{
+			ValueObjectConstructed: events.NewEvent(valueObjectConstructedEvent),
+		},
+	}
+}
+
+// IssueTransaction creates a new value object including tip selection and returns it.
+// It also triggers the ValueObjectConstructed event once it's done.
+func (v *ValueObjectFactory) IssueTransaction(tx *transaction.Transaction) *payload.Payload {
+	parent1, parent2 := v.tipManager.Tips()
+
+	valueObject := payload.New(parent1, parent2, tx)
+	v.Events.ValueObjectConstructed.Trigger(valueObject)
+
+	return valueObject
+}
+
+// ValueObjectFactoryEvents represent events happening on a ValueObjectFactory.
+type ValueObjectFactoryEvents struct {
+	// Fired when a value object is built including tips.
+	ValueObjectConstructed *events.Event
+}
+
+func valueObjectConstructedEvent(handler interface{}, params ...interface{}) {
+	handler.(func(*transaction.Transaction))(params[0].(*transaction.Transaction))
+}
diff --git a/dapps/valuetransfers/packages/tipmanager/events.go b/dapps/valuetransfers/packages/tipmanager/events.go
new file mode 100644
index 0000000000000000000000000000000000000000..13a4c05e28bf239b9e9b2f144aabbe7d2723d7f3
--- /dev/null
+++ b/dapps/valuetransfers/packages/tipmanager/events.go
@@ -0,0 +1,18 @@
+package tipmanager
+
+import (
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload"
+	"github.com/iotaledger/hive.go/events"
+)
+
+// Events represents events happening on the TipManager.
+type Events struct {
+	// Fired when a tip is added.
+	TipAdded *events.Event
+	// Fired when a tip is removed.
+	TipRemoved *events.Event
+}
+
+func payloadIDEvent(handler interface{}, params ...interface{}) {
+	handler.(func(payload.ID))(params[0].(payload.ID))
+}
diff --git a/dapps/valuetransfers/packages/tipmanager/tipmanager.go b/dapps/valuetransfers/packages/tipmanager/tipmanager.go
new file mode 100644
index 0000000000000000000000000000000000000000..edb1ac8d1edddc0e1df8c6eb3413c19b37414eaf
--- /dev/null
+++ b/dapps/valuetransfers/packages/tipmanager/tipmanager.go
@@ -0,0 +1,83 @@
+package tipmanager
+
+import (
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload"
+	"github.com/iotaledger/goshimmer/packages/binary/datastructure"
+	"github.com/iotaledger/hive.go/events"
+)
+
+// TipManager manages liked tips and emits events for their removal and addition.
+type TipManager struct {
+	// tips are all currently liked tips.
+	tips   *datastructure.RandomMap
+	Events Events
+}
+
+// New creates a new TipManager.
+func New() *TipManager {
+	return &TipManager{
+		tips: datastructure.NewRandomMap(),
+		Events: Events{
+			TipAdded:   events.NewEvent(payloadIDEvent),
+			TipRemoved: events.NewEvent(payloadIDEvent),
+		},
+	}
+}
+
+// AddTip adds the given value object as a tip.
+func (t *TipManager) AddTip(valueObject *payload.Payload) {
+	objectID := valueObject.ID()
+	parent1ID := valueObject.TrunkID()
+	parent2ID := valueObject.BranchID()
+
+	if t.tips.Set(objectID, objectID) {
+		t.Events.TipAdded.Trigger(objectID)
+	}
+
+	// remove parents
+	if _, deleted := t.tips.Delete(parent1ID); deleted {
+		t.Events.TipRemoved.Trigger(parent1ID)
+	}
+	if _, deleted := t.tips.Delete(parent2ID); deleted {
+		t.Events.TipRemoved.Trigger(parent2ID)
+	}
+}
+
+// RemoveTip removes the given value object as a tip.
+func (t *TipManager) RemoveTip(valueObject *payload.Payload) {
+	objectID := valueObject.ID()
+
+	if _, deleted := t.tips.Delete(objectID); deleted {
+		t.Events.TipRemoved.Trigger(objectID)
+	}
+}
+
+// Tips returns two randomly selected tips.
+func (t *TipManager) Tips() (parent1ObjectID, parent2ObjectID payload.ID) {
+	tip := t.tips.RandomEntry()
+	if tip == nil {
+		parent1ObjectID = payload.GenesisID
+		parent2ObjectID = payload.GenesisID
+		return
+	}
+
+	parent1ObjectID = tip.(payload.ID)
+
+	if t.tips.Size() == 1 {
+		parent2ObjectID = parent1ObjectID
+		return
+	}
+
+	parent2ObjectID = t.tips.RandomEntry().(payload.ID)
+	// select 2 distinct tips if there's more than 1 tip available
+	for parent1ObjectID == parent2ObjectID && t.tips.Size() > 1 {
+		parent2ObjectID = t.tips.RandomEntry().(payload.ID)
+	}
+
+	return
+}
+
+// Size returns the total liked tips.
+func (t *TipManager) Size() int {
+	return t.tips.Size()
+}
diff --git a/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go b/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..f3d2db45b7b979aa6884ab74c7a7efbe1f262dba
--- /dev/null
+++ b/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go
@@ -0,0 +1,132 @@
+package tipmanager
+
+import (
+	"sync"
+	"sync/atomic"
+	"testing"
+
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction"
+	"github.com/iotaledger/hive.go/events"
+	"github.com/stretchr/testify/assert"
+)
+
+// TestTipManager tests the functionality of the TipManager.
+func TestTipManager(t *testing.T) {
+	tipManager := New()
+
+	// check if first tips point to genesis
+	parent1, parent2 := tipManager.Tips()
+	assert.Equal(t, payload.GenesisID, parent1)
+	assert.Equal(t, payload.GenesisID, parent2)
+
+	// create value object and add it
+	v := payload.New(payload.GenesisID, payload.GenesisID, createDummyTransaction())
+	tipManager.AddTip(v)
+
+	// check count
+	assert.Equal(t, 1, tipManager.Size())
+
+	// check if both reference it
+	parent1, parent2 = tipManager.Tips()
+	assert.Equal(t, v.ID(), parent1)
+	assert.Equal(t, v.ID(), parent2)
+
+	// create value object and add it
+	v2 := payload.New(payload.GenesisID, payload.GenesisID, createDummyTransaction())
+	tipManager.AddTip(v2)
+
+	// check count
+	assert.Equal(t, 2, tipManager.Size())
+
+	// attach new value object to previous 2 tips
+	parent1, parent2 = tipManager.Tips()
+	assert.Contains(t, []payload.ID{v.ID(), v2.ID()}, parent1)
+	assert.Contains(t, []payload.ID{v.ID(), v2.ID()}, parent2)
+	v3 := payload.New(parent1, parent2, createDummyTransaction())
+	tipManager.AddTip(v3)
+
+	// check that parents are removed
+	assert.Equal(t, 1, tipManager.Size())
+	parent1, parent2 = tipManager.Tips()
+	assert.Equal(t, v3.ID(), parent1)
+	assert.Equal(t, v3.ID(), parent2)
+}
+
+// TestTipManagerParallel tests the TipManager's functionality by adding and selecting tips concurrently.
+func TestTipManagerConcurrent(t *testing.T) {
+	numThreads := 10
+	numTips := 100
+	numSelected := 10
+
+	var tipsAdded uint64
+	countTipAdded := events.NewClosure(func(valueObjectID payload.ID) {
+		atomic.AddUint64(&tipsAdded, 1)
+	})
+	var tipsRemoved uint64
+	countTipRemoved := events.NewClosure(func(valueObjectID payload.ID) {
+		atomic.AddUint64(&tipsRemoved, 1)
+	})
+
+	var wg sync.WaitGroup
+	tipManager := New()
+	tipManager.Events.TipAdded.Attach(countTipAdded)
+	tipManager.Events.TipRemoved.Attach(countTipRemoved)
+
+	for t := 0; t < numThreads; t++ {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+
+			tips := make(map[payload.ID]struct{})
+			// add a bunch of tips
+			for i := 0; i < numTips; i++ {
+				v := payload.New(payload.GenesisID, payload.GenesisID, createDummyTransaction())
+				tipManager.AddTip(v)
+				tips[v.ID()] = struct{}{}
+			}
+			// add a bunch of tips that reference previous tips
+			for i := 0; i < numSelected; i++ {
+				v := payload.New(randomTip(tips), randomTip(tips), createDummyTransaction())
+				tipManager.AddTip(v)
+			}
+		}()
+	}
+
+	wg.Wait()
+
+	// check if count matches and corresponding events have been triggered
+	assert.EqualValues(t, numTips*numThreads+numSelected*numThreads, tipsAdded)
+	assert.EqualValues(t, 2*numSelected*numThreads, tipsRemoved)
+	assert.EqualValues(t, numTips*numThreads-numSelected*numThreads, tipManager.Size())
+}
+
+func randomTip(tips map[payload.ID]struct{}) payload.ID {
+	var tip payload.ID
+
+	for k := range tips {
+		tip = k
+	}
+	delete(tips, tip)
+
+	return tip
+}
+
+func createDummyTransaction() *transaction.Transaction {
+	return transaction.New(
+		// inputs
+		transaction.NewInputs(
+			transaction.NewOutputID(address.Random(), transaction.RandomID()),
+			transaction.NewOutputID(address.Random(), transaction.RandomID()),
+		),
+
+		// outputs
+		transaction.NewOutputs(map[address.Address][]*balance.Balance{
+			address.Random(): {
+				balance.New(balance.ColorIOTA, 1337),
+			},
+		}),
+	)
+}