How to use Value Objects in Content Entities
I think I understood the motivation behind the entity api.

The entity api is not an orm. It is a dbal. The entity definitions do several things: 


define the database schema
define the hydration to a concrete object.
define form handling
routing


What does this do with handling Value Objects?

Storing Value Objects

Let's assume we have the following interface, we want to create an entity for:

&lt;?php
interface Product
{
   public function getId(): int;
   public function setId( int $id );

   public function getTitle(): string;
   public function setTitle( string $title );

   public function getPrice(): Price;
   public function setPrice( Price $price );

}


Which means we got one value object Price, which has a value and a currency.

Store the value objects values as fields

When thinking about storing a value object in a table, you would normally define a table as follows:

+------------------------+-----------+---------------+
|id|title                |priceValue | priceCurrency |
+------------------------+-----------+---------------+
|1 |Hitchhiker           |4200       |USD            |
+------------------------+-----------+---------------+


In drupal you define the schema in the static baseFieldDefinitions method and in the annotation, though that mainly just says which table to use. Please note drupal requires an id and uuid field. 

&lt;?php
/**
 * @ContentEntityType(
 *   id = "product",
 *   label = @Translation("Product"),
 *   base_table = "product",
 *   entity_keys = {
 *     "id" = "id",
 *     "uuid" = "uuid",
 *   },
 * )
 */
class Product extends ContentEntityBase implements ContentEntityInterface
{
    ...

    public static function baseFieldDefinitions(EntityTypeInterface $entity_type)
    {

        // Standard field, used as unique if primary index.
        $fields['id'] = BaseFieldDefinition::create('integer')
           -&gt;setLabel(t('ID'))
           -&gt;setDescription(t('The ID of the Product entity.'))
           -&gt;setReadOnly(true);

        // Standard field, unique outside of the scope of the current project.
        $fields['uuid'] = BaseFieldDefinition::create('uuid')
           -&gt;setLabel(t('UUID'))
           -&gt;setDescription(t('The UUID of the Product entity.'))
           -&gt;setReadOnly(true);

        $fields['title'] = BaseFieldDefinition::create('string')
           -&gt;setLabel(t('Title'))
           -&gt;setDescription(t('The title of the Product entity'))
           -&gt;setSettings([
                'default_value' =&gt; '',
                'max_length' =&gt; 256,
                'text_processing' =&gt; 0
            ]);

        // define the necessary fields for our Price object

        $fields['price_value'] = BaseFieldDefinition::create('integer')
           -&gt;setLabel(t('Price Value'))
           -&gt;setDescription(t('How much does the product cost in the currencies smalles unit (e.g.cents)? '));

        $fields['price_currency'] = BaseFieldDefinition::create('string')
           -&gt;setLabel(t('Price Currency'))
           -&gt;setDescription(t('The currency the Price is given in'))
           -&gt;setSettings([
              'default_value'   =&gt; 'USD',
              'max_length'      =&gt; 3,
              'text_processing' =&gt; 0,
           ]);   

    }
}


Now that we have defined what the database should look like, let's take care of hydration

&lt;?php
/**
 * @ContentEntityType(
 *   id = "product",
 *   label = @Translation("Product"),
 *   base_table = "product",
 *   entity_keys = {
 *     "id" = "id",
 *     "uuid" = "uuid",
 *   },
 * )
 */
class Product extends ContentEntityBase implements ContentEntityInterface
{

    public function getId()
    {
       return $this-&gt;get('id')-&gt;get(0)-&gt;get('value')-&gt;getValue() ;
    }

    public function setId( $id )
    {
       $this-&gt;get('id')-&gt;setValue($id);
    }

    public function getTitle()
    {
       return $this-&gt;get('title')-&gt;get(0)-&gt;get('value')-&gt;getValue() ;
    }

    public function setTitle( $title )
    {
       $this-&gt;get('title')-&gt;setValue($title);
    }

    public function getPrice()
    {
       return new Price(
            $this-&gt;get('price_value')-&gt;get(0)-&gt;get('value')-&gt;getValue(),
            $this-&gt;get('price_currency')-&gt;get(0)-&gt;get('value')-&gt;getValue()
        );
    }

    public function setPrice( Price $price )
    {
       $this-&gt;get('price_value')-&gt;setValue($price-&gt;getValue());
       $this-&gt;get('price_currency')-&gt;setValue($price-&gt;getCurrency());
    }

    public static function baseFieldDefinitions(EntityTypeInterface $entity_type)
    {
        ...
    }
}


Storing a list of value objects

Let's say our product comes in different formats (I know no one would actually design it that way, I just can't come up with a better example)

&lt;?php
interface Product
{
   public function getId(): int;
   public function setId( int $id );

   public function getTitle(): string;
   public function setTitle( string $title );

   public function getPrice(): Price;
   public function setPrice( Price $price );

   public function getFormats(): array;
   public function setFormats( Format ..$formatset);

}


For the sake of brevity I will pretend a Format only has one property. 

as fields of the product table

If there is a known upper bound to the number of formats I could pretty much design my database as before

+--+---------------------+-----------+---------------+-------------+-------------+
|id|title                |price_value|price_currency |format_title1|format_title2|
+--+---------------------+-----------+---------------+-------------+-------------+
|1 |Hitchhiker           |4200       |USD            |hardcover    |balloon      |
+--+---------------------+-----------+---------------+-------------+-------------+


as a referenced table

It would be nicer to design it as two tables, that way it can easily grow without changing my database

+--+---------------------+-----------+---------------+
|id|title                |price_value|price_currency |
+--+---------------------+-----------+---------------+
|1 |Hitchhiker           |4200       |USD            |
+--+---------------------+-----------+---------------+

+----------+------------+
|product_id|format_title|
+----------+------------+
|1         |hardcover   |
+----------+------------+
|1         |balloon     |
+----------+------------+


And that somehow seems possible, but I didn't dig into that, yet.

serializing

The least nicest, but easiest solution in Drupal is serializing the list of formats

+--+---------------------+-----------+---------------+-----------------------------------+
|id|title                |price_value|price_currency |format_set_serialized              |
+--+---------------------+-----------+---------------+-----------------------------------+
|1 |Hitchhiker           |4200       |USD            | #!gobblediegook%&amp;                 |
+--+---------------------+-----------+---------------+-----------------------------------+


The hydration would then look like the following

&lt;?php
class Product extends ContentEntityBase implements ContentEntityInterface
{

    ...

    public function getFormats()
    {
        return unserialize(
            $this-&gt;get('format_set_serialized')-&gt;get(0)-&gt;get('value')-&gt;getValue()
        );
    }

    public function setFormats( format ...$formatSet )
    {
       $this-&gt;get('format_set_serialized')-&gt;setValue(serialize($formatSet));
    }

    ...
}


What about computed fields, custom type data and custom storage handlers?

These exist to put field specific hydration logic in the dbal part of your entity. 

The question is "Will someone access the data through the entity api (dbal layer) instead of your fully hydrated domain class?"

If the anyswer is yes, then consider writing a repository service, that is available everywhere. I imagine most use cases revolve around getting domain specific data into drupal's database. So circumventing the domain logic seems like a very bad idea.

If the answer is still yes, then go for it.

I tried following the process described here. It worked but I had to add functions, that were not mentioned and were not part of the interface (specifically postSave and preSave). Also I encountered some very unhelpful errors on the way. After I had a working solution I scraped it for the simpler process outlined above. Your mileage may vary.