Teil 1: Firebase Auth und Cloud Firestore

App-Entwicklung mit Firebase

Tilo Dickopp

© Golden Sikorka/Shutterstock.com

Unter dem Firebase-Label bietet Google verschiedene Dienste an, die uns bei der Entwicklung von Apps und Webseiten unter die Arme greifen. Wir konzentrieren uns dabei ganz auf die Entwicklung unserer App und delegieren jede Arbeit, die andernfalls mit dem Backend anfallen würde, an Firebase, das in der Google Cloud läuft.

Gründe, die für den Einsatz einer Backend-as-a-Service-Architektur sprechen, sind kürzere Entwicklungszeiten und ein (für uns) vereinfachter Betrieb. Von diesen Vorteilen profitieren wir vor allem dann, wenn die angebotenen Dienste gut zu unserer Anwendung passen. Falls es so etwas gibt, ist die archetypische Anwendung von Firebase eine Mehrbenutzeranwendung, in der Benutzer in Echtzeit miteinander kollaborieren. Videospiele sind dafür ein typisches Beispiel, aber es wurde auch schon ein Contentmanagementsystem auf Basis von Firebase umgesetzt.

Artikelserie

  • Teil 1: Firebase Auth und Cloud Firestore
  • Teil 2: Cloud Storage, Cloud Functions und ML

Jeder neue Entwickler im Google-Firestore-Team schreibt in der ersten Woche eine kleine Beispiel-App oder Webseite mit Firebase. Oft kommen dabei Apps heraus, die bekannten Social-Media-Angeboten wie Twitter oder Instagram nachempfunden sind, aber wir haben auch schon ein kollaboratives Tetris-Spiel, einen Editor zur Pair Programming und ein Wissensquiz gesehen. In diesem Tutorial entwickeln wir eine Android-Anwendung in Kotlin, mit der wir verschiedene Arten von Veranstaltungen bewerben und finden können: Wenn wir zum Beispiel für ein Feierabendkickertreffen noch einige Spieler brauchen, legen wir das Event auf der Karte ab und andere Benutzer, die sich für Veranstaltungen der Kategorie „Sport“ interessieren, bekommen dann eine Benachrichtigung (Abb. 1).

Abb. 1: Beispielanwendung

Firebase-Konsole

Bevor wir mit der Entwicklung beginnen können, legen wir in der Firebase-Konsole ein neues Projekt an. Ein Projekt ist eine Sammlung von Firebase-Diensten, die von mehreren Apps genutzt werden. So könnten wir uns später entscheiden, unsere Veranstaltungs-App auch als Webseite anzubieten. Die Android-App und die Webseite wären dann zwei Apps, die zum gleichen Projekt gehören. Das Firebase-Team bietet unter anderem SDKs für Android, iOS, JavaScript und Unity an, in der Community werden jedoch auch Lösungen für eine Vielzahl anderer Technologien, wie Flutter oder Arduino, entwickelt.

Wir legen unser Projekt an, indem wir in der Firebase-Konsole unter https://console.firebase.google.com auf Add Project drücken. In den folgenden Schritten wählen wir einen Namen für unser Projekt und entscheiden uns dann im nächsten Schritt zunächst gegen die Verwendung von Google Analytics. Daraufhin landen wir in der Konsole unseres Projekts (Abb. 2). Hier sehen wir, dass wir momentan den Spark Plan benutzen, d. h. wir müssen nie für die Verwendung von Firebase bezahlen, unterliegen aber einigen Einschränkungen bezüglich des verfügbaren Speicherplatzes und der Anzahl der erlaubten API-Aufrufe. Für Anwendungen mit bis zu einigen Hundert Benutzern ist das aber in vielen Fällen vollkommen ausreichend.

Abb. 2: Firebase-Konsole unseres Projekts

Jetzt ist es an der Zeit, in der Konsole über das Android-Symbol eine Android-App mit unserem Projekt zu verbinden: Nachdem wir uns für einen Paketnamen entschieden haben, wird eine Datei namens google-services.json für uns erzeugt und zum Herunterladen angeboten. Listing 1 zeigt ein Beispiel. Der entscheidende Teil ist der api_key, den die Android-App benutzt, um sich unserem Firebase-Projekt zuzuordnen. Nachdem wir diese Datei in unser Android-Projekt kopiert haben, müssen wir noch das Plug-in com.google.gms.google-services zu unserem Gradle-Build-File hinzufügen. Die Firebase-Konsole bietet eine bebilderte Erklärung zu diesen Schritten, die leicht nachvollziehbar sein sollte.

{
  "project_info": {
    "project_number": "1234567890",
    "project_id": "java-magazin-tutorial",
      ...
  },
  "client": [
    {
      "api_key": [ { "current_key": "A1B2C3D4E5F6G7H8I9J0" } ],
      ...
    }
  ],
  "configuration_version": "1"
}

In diesem Tutorial benutzen wir das Webinterface der Firebase-Konsole, da die Schritte damit zum Einstieg leichter verständlich sind und uns oft auch hilfreiche Querverweise angeboten werden. Wenn uns das Geklicke lästig wird, haben wir allerdings auch die Möglichkeit, alle hier besprochenen Aufgaben in der Kommandozeile durchzuführen. Mit dem Firebase CLI [5] können wir uns zum Beispiel Informationen über die gerade angelegte App anzeigen lassen (Listing 2).

$ firebase apps:list
√ Preparing the list of your Firebase apps
┌───────────────────┬────────────────────────────────────┬───────────┐
│ App Display Name  │ App ID                             │ Platform  │
├───────────────────┼────────────────────────────────────┼───────────│ 
│ Firebase Tutorial │ 1:1234567890:android:105c…2198b67a │ ANDROID   │
└───────────────────┴────────────────────────────────────┴───────────┘
 
1 app(s) total.

Das Kommandozeilentool ist auch gerade dann hilfreich, wenn wir während der Entwicklung unserer Firebase-Apps einen CI/CD-Server einsetzen.

Authentifizierung

Der Firebase-Auth-Dienst nimmt uns Benutzerkontenverwaltung und Authentifizierung ab. Dazu gehören auch einige Aufgaben, die von Hand nicht leicht korrekt zu implementieren sind, wie zum Beispiel die Anmeldeversuche per IP-Adresse zu beschränken, um unsere App vor Missbrauch zu schützen. Um Firebase Auth zu nutzen, sind zwei Schritte notwendig: Zuerst legen wir deklarativ fest, wie sich Benutzer unserer App registrieren und anmelden sollen, dann nehmen wir eine kleine Änderung am Programmcode vor.

Zurzeit können wir uns zwischen zwölf verschiedenen Authentifizierungsprovidern wie E-Mail und Passwort, SMS oder Google-Konto entscheiden. Im Hauptmenü der Konsole wählen wir dazu Authentication | Sign-in method und aktivieren den oder die Provider, die wir in unserer App erlauben wollen. In manchen Fällen können wir das Verhalten eines Providers auch noch unseren Wünschen anpassen. So können wir zum Beispiel den Text der E-Mail ändern, die Benutzer zur Bestätigung ihrer Adresse zugeschickt bekommen.

Eine besondere Stellung hat der Provider Anonymous, mit dem wir Benutzern ohne Anmeldung Zugang zu unserer App gewähren. Wenn wir diesen deaktivieren, zwingen wir Benutzer, sich anzumelden und ggf. zu registrieren. Im Umkehrschluss heißt das allerdings nicht, dass wir bei aktiviertem Anonymous-Provider automatisch der ganzen Welt Zugriff auf alle Bereiche unserer App gewähren. Wie wir weiter unten im Abschnitt über Firebase Rules sehen werden, können wir den Zugriff auf ausgewählte Daten an eine bestehende Anmeldung knüpfen.

Sobald wir die ersten Anmeldungen haben, können wir unter Authentication | Users eine (hoffentlich schnell länger werdende) Liste aller Benutzer unserer App einsehen. Dort lassen sich auch Passwörter zurücksetzen und Accounts löschen und deaktivieren. Bei einem gelöschten Account kann sich der Benutzer jederzeit wieder neu anmelden. Bei einem deaktivierten Account bleibt der Zugang gesperrt.

In unserer Android-App haben wir nun die Wahl, zum Log-in unser eigenes UI zu gestalten oder aber auf vorgefertigte Komponenten zurückzugreifen. Solche Komponenten stellt Google unter dem Namen Firebase UI Open Source zur Verfügung, die wir nun, wie folgt, zu unserem Gradle-Build-File hinzufügen:

dependencies {
  ...
  implementation 'com.firebaseui:firebase-ui-auth:6.1.0'
}

com.firebaseui:firebase-ui-auth ist transitiv von com.google.firebase:firebase-auth abhängig, das wir benutzen würden, wenn wir unser eigenes Log-in-UI entwickeln wollten.

Listing 3 zeigt, wie wir mit einer Instanz von FirebaseAuth prüfen, ob ein Benutzer angemeldet ist, und andernfalls mit FirebaseUI einen Intent erstellen, der den Benutzer zwingt, sich anzumelden. Da wir damit nun sicher sein können, dass wir immer eine gültige Benutzer-ID in der Hand haben, benutzen wir diese, um die Daten zu laden, die wir für den Benutzer anzeigen wollen.

import com.firebase.ui.auth.AuthUI
import com.google.firebase.auth.FirebaseAuth
...
 
private const val RC_SIGN_IN = 123
 
class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    val auth = FirebaseAuth.getInstance()
    if (auth.currentUser == null) {
      startActivityForResult(
        AuthUI.getInstance()
          .createSignInIntentBuilder()
          .setAvailableProviders(listOf(
              AuthUI.IdpConfig.EmailBuilder().build(),
              AuthUI.IdpConfig.PhoneBuilder().build(),
              ... // Weitere Provider
            )).build(),
        RC_SIGN_IN)
    } else {
      loadUserData(auth?.currentUser?.uid)
    }
  }
 
  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
    super.onActivityResult(requestCode, resultCode, data)
    when (requestCode) {
      RC_SIGN_IN -> {
        if (resultCode != Activity.RESULT_OK) {
          finish() // Zum Beispiel
        }
        val user = FirebaseAuth.getInstance().currentUser
        loadUserData(user?.uid)
      }
    }
  }
 
  private fun loadUserData(userId: String?) {
    ...
  }
}

Firestore

Im nächsten Schritt speichern wir die Informationen zu unseren Veranstaltungen und die Interessen unserer Benutzer in Google Cloud Firestore. Firestore ist eine NoSQL-Datenbank, die insbesondere im Zusammenspiel mit Firebase ihre Stärken zeigt:

  • Firestore verfügt über ein Datenmodell, bei dem die Dokumente im Mittelpunkt stehen. Objekte und Hierarchien lassen sich damit gleichermaßen gut abbilden.
  • Firestore garantiert weltweite Datenkonsistenz. Wenn zum Beispiel ein Benutzer in Finnland ein Dokument ändert, können sich Benutzer in Italien und New York sicher sein, diese Änderung beim nächsten Zugriff auf das Dokument zu sehen.
  • Die Firestore SDKs unterstützen einen Offlinemodus, wobei Dokumente, mit denen eine App arbeitet, lokal gecacht werden, sodass sie auch offline weiterhin gelesen und geschrieben werden können. Wenn die Netzwerkverbindung wiederhergestellt ist, kümmert sich Firestore um die Synchronisation.
  • Firestore hat zwei Lesemodi: Dokumente können entweder dann angefordert werden, wenn sie gebraucht werden, oder wir abonnieren eine Anfrage. Bei einem solchen Abo liefert Firestore wie eine klassische SQL-Anfrage zunächst den aktuellen Zustand der angeforderten Dokumente zurück. Im Folgenden werden wir dann aber über alle zukünftigen Änderungen per Callback informiert.
  • Die Antwortzeit für Anfragen skaliert proportional zur Größe der Ergebnismenge, nicht zu der Menge unserer gespeicherten Daten.

Um eine Firestore-Datenbank für unser Projekt anzulegen, gehen wir in der Konsole in das Database-Menü und drücken dann auf Create Database. In dem Dialog, der daraufhin erscheint, bestimmen wir zunächst, dass unsere Datenbank im Testmodus erzeugt werden soll. Damit hat zwar zunächst weltweit jeder, der unsere Projekt-ID kennt, Zugriff auf alle unsere Daten, aber wir werden uns um die Absicherung kümmern, bevor wir die App an Benutzer verteilen – und Experimentieren ist im Testmodus sehr viel leichter.

Im letzten Schritt müssen wir uns noch entscheiden, in welcher Region unsere Datenbank erzeugt werden soll. Eine gute Wahl ist eur3 (europe-west). Damit bestimmen wir als Heimat für unsere Daten mehrere europäische Google-Datenzentren. Benutzer in anderen Kontinenten können unsere App aber trotzdem benutzen, wir legen nur fest, wo unsere Daten gespeichert werden.

In unserem Gradle-Build-File deklarieren wir nun noch eine Abhängigkeit zu com.google.firebase:firebase-firestore. Danach können wir Firestore in unserer App benutzen.

Das Firestore-Datenmodell

Bevor wir Daten in unsere neu erstellte Datenbank schreiben, sollten wir uns Gedanken über die Datenstruktur unserer Beispielanwendung machen. Dazu müssen wir zunächst verstehen, wie das Datenmodell von Firestore im Allgemeinen aufgebaut ist. Die kleinste Einheit bilden Dokumente, die eine Sammlung von Schlüssel/Wert-Paaren sind. Im einfachsten Fall wird jedes Dokument in unserem Programm durch eine Java Map repräsentiert. Innerhalb eines Dokuments hat jeder Wert einen Datentyp. Neben Zahlen, Booleschen Werten und Strings werden auch Arrays und geschachtelte Dokumente unterstützt. Ein geschachteltes Dokument ist nichts anderes als eine Map in einer Map. Darüber hinaus gibt es einige spezielle Datentypen: Zeitstempel, Referenzen auf andere Dokumente und geographische Koordinaten.

Jedes Dokument hat eine String ID und ist Teil einer Sammlung von Dokumenten – auf Englisch Collection. Weiterhin können Dokumente wiederum Sammlungen von Dokumenten enthalten, sodass alle Daten in einer großen Hierarchie angeordnet sind (Abb. 3).

Abb. 3: Firestore-Datenmodell

Für Entwickler, die bisher vornehmlich relationale Datenbanken benutzt haben, ist der Umstieg auf Firestore oder andere NoSQL-Datenbanken oft nicht einfach. Es besteht die Versuchung, Sammlungen als Tabellen zu verstehen. Das ist aber nicht richtig. Jedes Dokument in einer Sammlung kann eine andere Struktur haben. Ein Beispiel dafür sind Vererbungshierarchien: So möchten wir vielleicht Dokumente, die für Entwickler und Projektleiter unterschiedliche Attribute haben, in derselben Sammlung „Mitarbeiter“ speichern. Auch können gleichnamige Attribute zweier Dokumente in derselben Sammlung unterschiedliche Datentypen haben.

Ein weiterer Unterschied, über den Neulinge oft stolpern, sind Joins: In relationalen Datenbanken werden Daten aus zwei oder mehr Tabellen mittels Joins ad hoc miteinander verbunden. In NoSQL-Datenbanken haben wir diese Möglichkeit meist nicht, auch Firestore bildet hier keine Ausnahme. Anstelle von Joins können wir unsere Daten von vornherein in Kinddokumenten oder -sammlungen organisieren. Oft ist es bei NoSQL-Datenbanken später noch schwieriger, mit einer unpassenden oder gar schlecht durchdachten Datenstruktur zu arbeiten als bei relationalen Datenbanken. Es lohnt sich also, etwas Zeit zu investieren und uns weiter mit gutem Design zu beschäftigen. Die Firestore-Dokumentation enthält einige wichtige Hinweise, aber eine der besten, frei verfügbaren Einführungen in NoSQL-Datenmodellierung findet sich in der Dokumentation zu MongoDB.

Nachdem wir nun mit der Theorie vertraut sind, können wir unsere Beispielanwendung mit Daten füllen. Für Benutzer, Veranstaltungen und Tags (z. B. „Sport“ oder „Kunst“) legen wir jeweils eine Sammlung an (Tabelle 1). In unserem Modell für Veranstaltungen akzeptieren wir eine kleine Ungenauigkeit: Obwohl ownerId auf ein Benutzerdokument verweist, benutzen wir den Datentyp String anstelle einer Referenz. Diese Entscheidung macht es uns leichter, Firestore zusammen mit Firebase Auth zu verwenden. Da beide Dienste auch unabhängig voneinander benutzt werden können, darf Firestore Auth seine Daten nicht in einem Firestore-spezifischen Format vorhalten. Wir erinnern uns, dass wir in Listing 3 eine Benutzer-ID als String an loadUserData übergeben bekommen haben. Diesen String benutzen wir sowohl als Dokumenten-ID für unsere Benutzerdokumente als auch für Werte von ownerId. Eine Alternative hierzu wäre es, die Klasse com.google.firebase.auth.FirebaseUser, aus der wir die Benutzer-ID entnommen haben, direkt zu benutzen.

Dokumentensammlung Attribute
Benutzer (users) tags: Array mit Dokumenten-Referenzen
Veranstaltungen (events) ownerId: String
description: String
startTime: Timestamp
endTime: Timestamp
location: GeoPoint
tag: Dokumenten-Referenz
Tags (tags) name: String

Tabelle 1: Sammlungen unserer Beispielanwendung

Daten lesen und schreiben

Im letzten Abschnitt haben wir gesehen, dass all unsere Dokumente eine statische Struktur haben. Wir benötigen also nicht die volle Flexibilität, die wir erhielten, wenn wir Dokumente in unserem Code als Maps behandelten. Die Firestore SDKs erlauben uns, in solchen Fällen Datenklassen oder POJOs zu benutzen, um nicht mit Maps herumhantieren zu müssen. Listing 4 zeigt, wie unsere Datenklasse für Veranstaltungen aussieht.

import com.google.firebase.Timestamp
import com.google.firebase.firestore.DocumentReference
import com.google.firebase.firestore.GeoPoint
 
data class Event(
  val ownerId: String? = null,
  val description: String? = null,
  val startTime: Timestamp? = null,
  val endTime: Timestamp? = null,
  val location: GeoPoint? = null,
  val tag: DocumentReference? = null
)

Zum Lesen und Schreiben von Dokumenten benötigen wir zunächst eine Instanz von FirebaseFirestore. Mit den Methoden collection und document erhalten wir dann eine CollectionReference oder DocumentReference. Diese Klassen haben wiederum Methoden für alle Datenbankoperationen. In Listing 5 erzeugen wir, wenn nötig, ein neues Benutzerdokument und bestimmen, dass sich jeder neue Benutzer zunächst für jede Art von Veranstaltung interessiert, also die komplette Liste aller Tags hat.

private fun loadUserData(userId: String?) {
  if (userId == null) {
    return
  }
  val firestore = FirebaseFirestore.getInstance()
  firestore.collection("users").document(userId).get()
    .addOnSuccessListener { userDoc ->
      if (userDoc != null) {
        // User tags in UI anzeigen
      } else {
        firestore.collection("tags").get()
          .addOnSuccessListener { tagCollection ->
            val newUserDoc = User(tagCollection.documents.map { it.reference })
            firestore.collection("users").document(userId).set(newUserDoc)
          }
      }
    }
}

Im letzten Beispiel benutzen wir addOnSuccessListener, um im Erfolgsfall mit dem gelesenen Dokument weiterarbeiten zu können. Der Listener wird also maximal einmal aufgerufen. Firestore erlaubt es uns jedoch, auch Abfrageergebnisse zu abonnieren. In diesem Fall wird der entsprechende Listener immer aufgerufen, wenn sich die Daten geändert haben. In Listing 6 abonnieren wir die Sammlung von Veranstaltungen, sodass die Markierungen in unserer Karte immer aktuell sind und wir Benutzer über neue, interessante Veranstaltungen unterrichten können.

firestore.collection("events").whereIn("tag", subscribedTags)
  .addSnapshotListner { snapshots, exception  ->
    if (exception != null || snapshots == null) {
      return@addSnapshotListener
    }
    for (change in snapshots.documentChanges) {
      when(change.type) {
        ADDED -> {
          addMapMarker(change.document)
        }
        REMOVED -> {
          removeMapMarker(change.document)
        }
        CHANGED -> {
          removeMapMarker(change.document)
          addMapMarker(change.document)
        }
      }
    }
  }

Damit Firestore alle diese Operationen skalierbar ausführen kann, müssen wir für jede Art von Anfrage einen Index erstellen. Das können wir entweder von Hand in der Konsole unter Database | Indexes erledigen oder wir schicken die Anfrage ohne einen entsprechenden Index an Firestore. Daraufhin wird eine Exception geworfen, die einen Link enthält, mit dem Firestore den benötigten Index für uns erstellt.

Firebase Rules

Um unsere Daten zu schützen, müssen wir nun noch für jedes Dokument festlegen, wer Lese- und wer Schreibrechte hat. Firestore benutzt dafür Attribute Based Access Control (ABAC) (Kasten: „RBAC vs. ABAC“) und eine domänenspezifische Sprache: Firebase Rules. Für jede Sammlung – wenn wir wollen auch für jedes Dokument – schreiben wir unter Database | Rules eine Regel, die festlegt, wer Zugriff erhält.

Listing 7 zeigt die drei folgenden Regeln, die für unseren Fall gut geeignet sind:

  • Benutzerdokumente dürfen nur vom Benutzer selbst geändert werden. Das betrifft die Tags/Interessen, die wir ja im Benutzerdokument gespeichert haben.
  • Tags dürfen von jedem angemeldeten Benutzer gelesen, aber von niemandem geschrieben werden. Als Administratoren können wir die Liste der Tags trotzdem in der Konsole unter Database | Data ändern.
  • Veranstaltungen dürfen von jedem angemeldeten Benutzer gelesen und hinzugefügt werden, aber nur der Benutzer, der eine Veranstaltung angelegt hat, darf diese ändern oder löschen.
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{user} {
      allow read, write: if user == request.auth.uid;
    }
    
    match /tags/{tag} {
      allow read: if request.auth != null;
      allow write: if false;
    }
    
    match /events/{event} {
      allow read, create: if request.auth != null;
      allow update, delete:
        if request.auth.uid ==
          get(/databases/$(database)/documents/events/$(event)).data.ownerId;
    }
  }
}

RBAC vs. ABACIn einer klassischen Drei-Schichten-Architektur ist die Aufgabenverteilung meist so gestaltet, dass die Datenbank dem Server Daten bereitstellt und dieser für die Applikationslogik verantwortlich ist: die eigentliche Geschäftslogik, Autorisierung usw. Zur Autorisierung ordnet der Server dem Benutzer (z. B. Bob) oder der Benutzergruppe (z. B. Admins) eine Rolle (z. B. Accountverwalter) zu und entscheidet dann aufgrund der Rolle, ob Zugriff auf eine bestimmte Ressource gewährt werden darf. Dieses Model wird im Englischen als Role Based Access Control (RBAC) bezeichnet.

Viele Firebase-Anwendungen sind aber severless: Das Frontend – in unserem Beispiel eine Android-App – kommuniziert direkt mit der Firestore-Datenbank. Die klassischen Aufgaben des Servers müssen also zwischen dem Frontend und der Datenbank aufgeteilt werden. Die Geschäftslogik kann oft ohne allzu große Probleme in die App portiert werden, aber für die Autorisierungslogik ist das aus Sicherheitsgründen keine Option – die Datenbank ist hierfür der einzige Ort.

Frei gestaltbare RBAC würde es nun erfordern, unsere Datenbank programmierbar zu machen. Eine bessere Alternative bietet hier Attribute Based Access Control (ABAC). Dabei werden aus verschiedenen Attributen Boolesche Ausdrücke erstellt, die über Zugriff auf Ressourcen entscheiden.

Beispiele für Attribute sind:

  • request.auth
  • resource.metadata
  • system.time

Ausdrücke, die diese Attribute verwenden, könnten dann sein:

  • request.auth.isAdmin
  • system.time > 5 && system.time < 17

Da Benutzer, Ressourcen und Rollen auch Attribute sein können, ist leicht ersichtlich, dass wir durch ABAC nicht eingeschränkt werden.

Fazit

Im ersten Teil dieses Tutorials haben wir gesehen, wie wir mit Hilfe von Firebase in kürzester Zeit eine voll funktionsfähige Android-App entwickeln können. Dazu haben wir, wo immer möglich, auf fertige Dienste und Komponenten zurückgegriffen. Sich komplett auf Clouddienste zu verlassen, mag nicht in allen Fällen wünschenswert sein, aber in Zeiten immer schneller aufeinanderfolgender Entwicklungszyklen hilft es uns, mit Backend-as-a-Service-Architekturen umgehen zu können und entsprechende Entwürfe in unserem Repertoire zu haben.

Geschrieben von
Tilo Dickopp

Tilo Dickopp arbeitet als Softwareentwickler bei Google in San Francisco. Seine Interessenschwerpunkte sind datenintensive und verteilte Systeme. Momentan ist er für die Persistenzschicht von Cloud Firestore verantwortlich.

Kommentare

Hinterlasse einen Kommentar

avatar
4000
  Subscribe  
Benachrichtige mich zu: