One aspect I keep hammering on about with regard to microservices is reusability. If you're developing just one user-facing product, nevermind. But if you have a bunch of products, reusability across microservices is a huge boon compared to monoliths. Reuse all your job processing, notifications, data storage, login/auth/group/role/permission management, identity verification, image processing, NLP processing, feed processing, etc. etc. etc. across your entire stack.
With monoliths, the only other option is to build these as libraries, and unless you want to write bindings or write code more than once, you're now chaining yourself to a specific language.
We use a simple microservice design: All our microservices talk using JSON over HTTP. They share nothing. Everything is done by communicating. There's also an event bus so they can listen to each other's events. All our microservices are multitenant by design, so that they can support any product with no data spillover.
We have a bunch of general-purpose microservices built out to service our particular needs. User-facing webapps are built entirely out of these building blocks. They don't need to deal with login or OAuth; there's a microservice for that. Similarly, almost all data is stored in a single document-oriented data store (which we've heavily rearchitecting in Go and will soon release as open source — email me if you want to be notified) on top Postgres and Elasticsearch. For anything that we need from a webapp, we build a microservice. (We have a lot of older Ruby and Node stuff, but all our new microservices are written in Go, which has been a very pleasant experience.)
One key component of our development workflow is the use of a Linux virtual machine that each developer runs locally. It hosts the exact same stack (and uses the same deployment system) as our real production and staging environments. This lets the developer focus on work and not on how the microservices are run. It's not pain-free, but we have made it quite comfortable, among other things through a seamless system of mounting, where the code running on the VM for a particular app is using the developer's own project folder on the host machine. Hot code reloading is also enabled where possible. So a React app will automatically bundle with Babel and so on when you reload a page.
There are challenges to using microservices in this way. One is deployment and hosting. We currently have our own homegrown system for this that works fine, but hope to migrate to Kubernetes soon. Another challenge is documentation. In the end, the code (and project readmes) is the authority on how a particular microservice works and what APIs it implements; we did automatically generate API docs at one point, but it hasn't kept up with our Go projects. These days we just refer to the code. The most painful aspect of this, and onboarding, is documenting the cross-cutting, "big picture" concerns.
I'd be happy to answer any other questions you might have.
Edit: how do you scale? For example, do you just have lots of nodes running your authentication microservice for all of your active apps?
All our webapps these days are thin React apps (with server-side rendering) that don't have a dedicated backend. Instead, they talk to a group of generic microservices. We've been doing this style of development since around 2010.
With this methodology, a lot of Rails' ergonomic concerns (templating, the split between rendering HTML vs. data, etc.) just melt away. Front-ends become formally isolated clients of microservices, forcing you to engineer better internal APIs and get things like security correct from the start.