I have a simple normalized model for invoices and payments.
The relevant fields are
has_many :payments, through: :invoice_payments
has_many :invoices, through: :invoice_payments
invoices.where('amount_outstanding > 0')
invoices = invoices.left_outer_joins(:invoice_payments)
invoices = invoices.distinct.select('invoices.*, sum(invoice_payments.amount) as total_payments')
invoices = invoices.group('invoices.id')
invoices = invoices.having('sum(invoice_payments.amount) < invoices.amount_with_tax or sum(invoice_payments.amount) is null')
<%[email protected] %>
paid = invoice_payments.sum(:amount)
self.amount_outstanding = self.amount_with_tax - paid
It depends a lot on use cases that display or rely on amount outstanding. How often do you use this value and will the users accept little delay (considering that it would be slow to compute this value) just to display this value while you can still display all other information on the page promptly?
You mentioned you have not done performance testing for this scenario and also there is no information on the scope of system in terms of number of invoices processed. As with any financial system, all the transactions tend to be associated with a particular financial period, so its more appropriate to consider that period instead of all the invoices stored in the system to access any performance needs.
In general, the processing that is required without denormalized field should be able to handle quite a large number of invoices and of course it will depend on your server setup too. Also there is a possibility that you can still optimize the code and reduce the number of queries to get the same functionality.
If you haven't already seen the issues with performance, you might be attempting premature optimization which is not always a best practice. So I would consider avoiding denormalized field until I see the need for it and then do the necessary optimization as and when needed. Hope that helps.