Correct way to validate config entities?
It depends on how your config-entities are created.
If the only way to create them is via a form, then you could just override the validateForm method in your form to ensure the entity is valid. In this context the entity should be available by running this code

$form_state-&gt;cleanValues();
$this-&gt;entity = $this-&gt;buildEntity($form, $form_state);


This is what content entity forms do (see \Drupal\Core\Entity\ContentEntityForm::validateForm).

However if you expect your entities to be added programatically or via REST and you need lower-level validation, then you could add a validate method to you entity and call this from the preSave method. Having it available as a public method (validate) would allow other consumers of your API to check if the entity was valid before they called save, thus avoiding the exception.

In terms of the contents of the validate method, you have lots of options. You could try and piggy-back onto core's constraint system - which uses plugins to define constraints. Or you could keep it simple and just return an array of errors. Finally you could keep use a middle-ground and just integrate with the validator component in the Symfony way, which would at least keep your validate return consistent with those for Content entities - something like so:

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Validation;

/**
 * Validates my config entity.
 *
 * @return \Symfony\Component\Validator\ConstraintViolationList 
 *   List of violations. Empty if valid.
 */
public function validate() {
  $validator = Validation::createValidator();
    -&gt;addMethodMapping('loadValidatorMetadata')
    -&gt;getValidator();
  return $validator-&gt;validate($this);
}

/**
 * Defines validation rules for this config entity.
 */
public static function loadValidatorMetadata(ClassMetadata $metadata) {
  // Same validator rules.
  // Add your own as required.
  // Note this is static, so you don't have $this in scope.
  $metadata-&gt;addPropertyConstraint('someProperty', new Assert\NotBlank(['message' =&gt; 'Some property cannot be blank']));
  // But if you add a validation callback, you can operate in the context
  // of an instance.
  $metadata-&gt;addConstraint(new Assert\Callback(['methods' =&gt; ['someValidationCallback']]));
}

/**
 * Validation callback for some field.
 */
public function someValidationCallback(ExecutionContextInterface $context) {
  if ($this-&gt;something === 'something it should not be') {
      $context-&gt;buildViolation("Can't let you do that Dave.")
      -&gt;atPath('something')
      -&gt;addViolation();
    }
}