Configurable fields are typically created through the UI, attached to an entity type bundle, and exported to code. The part highlighted with bold is a critical difference between these and base fields in that base fields exist on all bundles of the entity type. You should already be familiar with the UI for creating a configurable field:
They also use the TypedData API for their definitions, as well as the same field type, widget, and formatter plugins we talked about earlier. Architecturally speaking, the main difference between base and configurable fields is that the latter are made up of two parts: storage configuration (FieldStorageConfig) and field configuration (FieldConfig). These are both configuration entity types, whose entities, together, make up a configurable field. The former defines the field settings that relate to how the field is stored. These are options that apply to that particular field across all the bundles of an entity type it may be attached to (such as cardinality, the field type, and so on). The latter defines options for the field specific to the bundle it is attached to. These can, in some cases, be overrides of the storage config but also new settings (such as the field description, whether it is required, and more).
The easiest way to create configurable fields is through the UI. Just as easily, you get them exported into code. You could alternatively write the field storage configuration and field configuration yourself and add it to your module's config/install folder, but you can achieve the same if you just export them through the UI.
Moreover, you can use a couple of hooks to make alterations to existing fields. For example, by implementing hook_entity_field_storage_info_alter() you can alter field storage configurations, while with hook_entity_bundle_field_info_alter() you can alter field configurations as they are attached to an entity type bundle.