How to apply a custom template for posts by default?

single.php template customization from a child theme:

If the main theme you’re going to upgrade in future is not a child theme itself, then you can create your own child theme and create a single.php template file within the child theme to override the parent theme’s single.php. This way future upgrades of the parent theme will not replace your customizations.

What if creating a child theme is not an option?

Say you don’t want to create a child theme for some valid reasons or your theme itself is a child theme and you want to upgrade it in the future. As WordPress doesn’t support Grand Child theme, you have a couple of options:

1. Using a higher priority template than single.php:

According to WordPress Template Hierarchy, you can use single-{post-type}.php template file to override single.php.

Since you are going to use the new template by default for posts, if you create a new template file and name it single-post.php, then by default all posts will be loaded with this template instead of single.php because of how it’s defined in Template Hierarchy of WordPress core.

2. Using a Plugin to override single.php template for posts:

Say even single-post.php is already available in your theme or you suspect it may be added in future upgrades, in that case, the only alternative you have is to add a custom plugin to change this behaviour.

You may use the single_template filter hook to achieve that. Sample plugin CODE:

<?php
/*
Plugin Name:  Custom Theme Utility Plugin
Plugin URI:   https://wordpress.stackexchange.com/a/307541/110572
Description:  Overrides single.php template for posts by default
Version:      1.0.0
Author:       Fayaz Ahmed
Author URI:   https://www.fayazmiraz.com/
*/

define( 'WPSE_307430_PLUGIN_TEMPLATE_DIR', plugin_dir_path( __FILE__ ) );

function wpse307430_override_single_template( $template_file, $type, $templates ) { 
    global $post;

    if ( $post->post_type == 'post' ) { 
        $custom_single_template = WPSE_307430_PLUGIN_TEMPLATE_DIR . 'single.php';
        $template_length = strlen( $template_file );
        // To determine if we want to override single.php from plugin
        // Without this check, WordPress template hierarchy will be broken for single posts, we don't want that
        $single_override = $template_length === 0 || substr_compare( $template_file, "single.php", strlen( $template_file ) - 10, 10) === 0;
        if( $single_override && file_exists( $custom_single_template ) ) { 
            $template_file = $custom_single_template;
        }   
    }   

    return $template_file;
}
add_filter( 'single_template', 'wpse307430_override_single_template', 10, 3 );

Create this sample plugin file within a directory (for example: /theme-util/theme-util.php) under plugins directory, activate the plugin and keep your custom single.php template file within this plugin directory as /theme-util/single.php.

This way as long as this plugin is active, the single.php template file within this plugin directory will override single.php template file from both a parent theme and a child theme.